From ff8fb7e4b0ea4a1abc7a8f101dd5209c5778b7d1 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 3 Feb 2020 11:48:03 +0100 Subject: [PATCH] Sync docs from vGreenwich.SR5 to gh-pages --- Greenwich.SR5/css/highlight.css | 35 + Greenwich.SR5/css/manual-multipage.css | 9 + Greenwich.SR5/css/manual-singlepage.css | 6 + Greenwich.SR5/css/manual.css | 342 + Greenwich.SR5/ghpages.sh | 348 + Greenwich.SR5/images/.gitkeep | 0 Greenwich.SR5/images/Deps.png | Bin 0 -> 37192 bytes Greenwich.SR5/images/Hystrix.png | Bin 0 -> 230655 bytes Greenwich.SR5/images/HystrixFallback.png | Bin 0 -> 44880 bytes Greenwich.SR5/images/HystrixGraph.png | Bin 0 -> 39829 bytes Greenwich.SR5/images/RequestLatency.png | Bin 0 -> 14722 bytes Greenwich.SR5/images/SCSt-groups.png | Bin 0 -> 17392 bytes Greenwich.SR5/images/SCSt-overview.png | Bin 0 -> 96212 bytes Greenwich.SR5/images/SCSt-partitioning.png | Bin 0 -> 18068 bytes Greenwich.SR5/images/SCSt-sensors.png | Bin 0 -> 16910 bytes Greenwich.SR5/images/SCSt-with-binder.png | Bin 0 -> 18899 bytes Greenwich.SR5/images/Stubs1.png | Bin 0 -> 35170 bytes Greenwich.SR5/images/Stubs2.png | Bin 0 -> 18143 bytes Greenwich.SR5/images/background.png | Bin 0 -> 18255 bytes Greenwich.SR5/images/callouts/1.png | Bin 0 -> 329 bytes Greenwich.SR5/images/callouts/2.png | Bin 0 -> 353 bytes Greenwich.SR5/images/callouts/3.png | Bin 0 -> 350 bytes Greenwich.SR5/images/caution.png | Bin 0 -> 2099 bytes .../custom_vs_global_error_channels.png | Bin 0 -> 48392 bytes Greenwich.SR5/images/dependencies.png | Bin 0 -> 24825 bytes Greenwich.SR5/images/important.png | Bin 0 -> 2085 bytes Greenwich.SR5/images/kibana.png | Bin 0 -> 186956 bytes Greenwich.SR5/images/logo.png | Bin 0 -> 4387 bytes Greenwich.SR5/images/note.png | Bin 0 -> 2257 bytes Greenwich.SR5/images/parents.png | Bin 0 -> 10300 bytes Greenwich.SR5/images/part-bindings.png | Bin 0 -> 61765 bytes Greenwich.SR5/images/part-exchange.png | Bin 0 -> 24024 bytes Greenwich.SR5/images/part-queues.png | Bin 0 -> 46910 bytes Greenwich.SR5/images/producers-consumers.png | Bin 0 -> 15947 bytes Greenwich.SR5/images/pws.png | Bin 0 -> 13784 bytes Greenwich.SR5/images/rabbit-binder.png | Bin 0 -> 12440 bytes Greenwich.SR5/images/redis-binder.png | Bin 0 -> 13731 bytes Greenwich.SR5/images/registration.png | Bin 0 -> 22405 bytes Greenwich.SR5/images/schema_reading.png | Bin 0 -> 45621 bytes Greenwich.SR5/images/schema_resolution.png | Bin 0 -> 27718 bytes ...spring-cloud-launcher-eureka-dashboard.png | Bin 0 -> 112368 bytes .../images/spring-cloud-launcher-log.png | Bin 0 -> 281927 bytes Greenwich.SR5/images/stream-initializr.png | Bin 0 -> 215901 bytes Greenwich.SR5/images/tip.png | Bin 0 -> 931 bytes Greenwich.SR5/images/trace-id.png | Bin 0 -> 86175 bytes Greenwich.SR5/images/warning.png | Bin 0 -> 2130 bytes .../images/zipkin-error-trace-screenshot.png | Bin 0 -> 211221 bytes Greenwich.SR5/images/zipkin-error-traces.png | Bin 0 -> 118844 bytes .../images/zipkin-trace-screenshot.png | Bin 0 -> 169485 bytes Greenwich.SR5/images/zipkin-traces.png | Bin 0 -> 152670 bytes Greenwich.SR5/images/zipkin-ui.png | Bin 0 -> 111016 bytes Greenwich.SR5/multi/css/highlight.css | 35 + Greenwich.SR5/multi/css/manual-multipage.css | 9 + Greenwich.SR5/multi/css/manual-singlepage.css | 6 + Greenwich.SR5/multi/css/manual.css | 342 + Greenwich.SR5/multi/images/background.png | Bin 0 -> 18255 bytes Greenwich.SR5/multi/images/callouts/1.png | Bin 0 -> 329 bytes Greenwich.SR5/multi/images/callouts/2.png | Bin 0 -> 353 bytes Greenwich.SR5/multi/images/callouts/3.png | Bin 0 -> 350 bytes Greenwich.SR5/multi/images/caution.png | Bin 0 -> 2099 bytes Greenwich.SR5/multi/images/important.png | Bin 0 -> 2085 bytes Greenwich.SR5/multi/images/logo.png | Bin 0 -> 4387 bytes Greenwich.SR5/multi/images/note.png | Bin 0 -> 2257 bytes Greenwich.SR5/multi/images/tip.png | Bin 0 -> 931 bytes Greenwich.SR5/multi/images/warning.png | Bin 0 -> 2130 bytes ...y_of_springs_data_integration_journey.html | 9 + Greenwich.SR5/multi/multi__actuator_api.html | 59 + .../multi/multi__additional_resources.html | 4 + ...addressing_all_instances_of_a_service.html | 6 + .../multi/multi__addressing_an_instance.html | 12 + .../multi/multi__apache_kafka_binder.html | 311 + .../multi__apache_kafka_streams_binder.html | 276 + ...ompendium_of_configuration_properties.html | 3 + .../multi/multi__binder_implementations.html | 3 + .../multi__broadcasting_your_own_events.html | 34 + Greenwich.SR5/multi/multi__building.html | 47 + ...e_gateway_using_spring_mvc_or_webflux.html | 31 + ...ulti__building_and_running_a_function.html | 18 + Greenwich.SR5/multi/multi__bus_endpoints.html | 13 + ...ulti__circuit_breaker_hystrix_clients.html | 61 + ...ti__circuit_breaker_hystrix_dashboard.html | 4 + .../multi/multi__client_side_usage_2.html | 66 + Greenwich.SR5/multi/multi__cloud_foundry.html | 8 + ...entity_aware_proxy_iap_authentication.html | 13 + .../multi__cloud_memorystore_for_redis.html | 15 + .../multi__cloud_native_applications.html | 9 + .../multi/multi__configuration_2.html | 63 + .../multi/multi__configuration_options.html | 127 + ...entication_downstream_of_a_zuul_proxy.html | 17 + ...actories_and_gateway_filter_factories.html | 24 + Greenwich.SR5/multi/multi__contributing.html | 88 + .../multi/multi__cors_configuration.html | 13 + Greenwich.SR5/multi/multi__current_span.html | 19 + .../multi__current_tracing_component.html | 7 + Greenwich.SR5/multi/multi__customization.html | 217 + .../multi/multi__customizations.html | 101 + ...multi__customizing_the_message_broker.html | 13 + .../multi/multi__dependency_management.html | 15 + .../multi__deploying_a_packaged_function.html | 3 + .../multi/multi__developer_guide.html | 98 + Greenwich.SR5/multi/multi__discovery.html | 22 + ...multi__discoveryclient_for_kubernetes.html | 21 + .../multi/multi__dynamic_compilation.html | 23 + .../multi__embedding_the_config_server.html | 26 + Greenwich.SR5/multi/multi__examples_2.html | 5 + ...ulti__external_configuration_archaius.html | 20 + Greenwich.SR5/multi/multi__features.html | 4 + Greenwich.SR5/multi/multi__features_2.html | 139 + ...alog_and_flexible_function_signatures.html | 39 + .../multi__functional_bean_definitions.html | 81 + .../multi/multi__gatewayfilter_factories.html | 434 + .../multi/multi__getting_started.html | 9 + .../multi/multi__getting_started_2.html | 5 + .../multi/multi__global_filters.html | 79 + Greenwich.SR5/multi/multi__glossary.html | 3 + .../multi/multi__google_cloud_pubsub.html | 76 + .../multi/multi__google_cloud_vision.html | 27 + .../multi/multi__health_indicator_5.html | 5 + Greenwich.SR5/multi/multi__http_clients.html | 7 + .../multi/multi__httpheadersfilters.html | 3 + ...__hystrix_timeouts_and_ribbon_clients.html | 63 + .../multi/multi__instrumentation.html | 6 + Greenwich.SR5/multi/multi__integrations.html | 183 + ...ulti__inter_application_communication.html | 37 + Greenwich.SR5/multi/multi__introduction.html | 291 + .../multi/multi__introduction_2.html | 27 + .../multi/multi__introduction_4.html | 3 + .../multi/multi__kotlin_support.html | 5 + ...multi__kubernetes_ecosystem_awareness.html | 16 + ...__kubernetes_native_service_discovery.html | 4 + ...rnetes_propertysource_implementations.html | 223 + .../multi/multi__leader_election.html | 3 + Greenwich.SR5/multi/multi__links.html | 6 + Greenwich.SR5/multi/multi__main_concepts.html | 35 + ...ulti__managing_spans_with_annotations.html | 45 + Greenwich.SR5/multi/multi__migrations.html | 96 + .../multi__modules_in_maintenance_mode.html | 5 + Greenwich.SR5/multi/multi__more_detail.html | 120 + Greenwich.SR5/multi/multi__naming_spans.html | 34 + .../multi/multi__other_resources.html | 3 + .../multi/multi__pod_health_indicator.html | 4 + .../multi__polyglot_support_with_sidecar.html | 59 + .../multi/multi__programming_model.html | 395 + Greenwich.SR5/multi/multi__propagation.html | 96 + ...sh_notifications_and_spring_cloud_bus.html | 11 + Greenwich.SR5/multi/multi__quick_start.html | 83 + Greenwich.SR5/multi/multi__quick_start_2.html | 53 + Greenwich.SR5/multi/multi__quick_start_3.html | 23 + Greenwich.SR5/multi/multi__quick_start_4.html | 37 + Greenwich.SR5/multi/multi__quickstart.html | 72 + .../multi/multi__rabbitmq_binder.html | 428 + .../multi__reactor_netty_access_logs.html | 17 + ...multi__ribbon_discovery_in_kubernetes.html | 20 + .../multi/multi__router_and_filter_zuul.html | 494 + .../multi/multi__running_examples.html | 7 + Greenwich.SR5/multi/multi__sample_13.html | 3 + Greenwich.SR5/multi/multi__samples.html | 3 + Greenwich.SR5/multi/multi__sampling.html | 53 + ...rity_configurations_inside_kubernetes.html | 9 + .../multi/multi__sending_spans_to_zipkin.html | 28 + .../multi__serverless_platform_adapters.html | 49 + ...lti__service_discovery_eureka_clients.html | 144 + .../multi__service_id_must_be_unique.html | 9 + ...multi__service_registry_configuration.html | 17 + ...ulti__service_registry_implementation.html | 5 + .../multi__serving_alternative_formats.html | 9 + .../multi/multi__serving_plain_text.html | 29 + .../multi/multi__single_sign_on_2.html | 11 + .../multi/multi__span_lifecycle.html | 62 + .../multi/multi__spring_cloud_bus.html | 8 + ...ing_cloud_commons_common_abstractions.html | 344 + .../multi/multi__spring_cloud_config.html | 7 + .../multi/multi__spring_cloud_config_2.html | 35 + .../multi__spring_cloud_config_client.html | 83 + .../multi__spring_cloud_config_server.html | 493 + .../multi/multi__spring_cloud_consul.html | 9 + ..._context_application_context_services.html | 98 + .../multi/multi__spring_cloud_contract.html | 4 + .../multi/multi__spring_cloud_contract_2.html | 7 + .../multi__spring_cloud_contract_faq.html | 702 + ...ti__spring_cloud_contract_stub_runner.html | 895 + ..._cloud_contract_verifier_introduction.html | 843 + ...ing_cloud_contract_verifier_messaging.html | 260 + ..._spring_cloud_contract_verifier_setup.html | 7 + ...multi__spring_cloud_contract_wiremock.html | 322 + ...multi__spring_cloud_for_cloud_foundry.html | 18 + .../multi/multi__spring_cloud_function_2.html | 3 + .../multi/multi__spring_cloud_gateway.html | 3 + .../multi/multi__spring_cloud_kubernetes.html | 3 + .../multi/multi__spring_cloud_netflix.html | 8 + .../multi/multi__spring_cloud_openfeign.html | 4 + .../multi/multi__spring_cloud_security.html | 11 + .../multi/multi__spring_cloud_sleuth.html | 3 + .../multi/multi__spring_cloud_sleuth_2.html | 27 + .../multi/multi__spring_cloud_stream.html | 3 + .../multi/multi__spring_cloud_stream_2.html | 21 + .../multi/multi__spring_cloud_vault.html | 3 + .../multi/multi__spring_cloud_zookeeper.html | 9 + .../multi__spring_data_cloud_datastore.html | 467 + .../multi__spring_data_cloud_spanner.html | 465 + .../multi/multi__spring_integration.html | 113 + Greenwich.SR5/multi/multi__spring_jdbc.html | 42 + .../multi/multi__spring_resources.html | 18 + .../multi/multi__stackdriver_logging.html | 60 + ...ti__standalone_streaming_applications.html | 4 + .../multi__standalone_web_applications.html | 16 + Greenwich.SR5/multi/multi__starters.html | 22 + Greenwich.SR5/multi/multi__testing.html | 56 + Greenwich.SR5/multi/multi__tls_ssl.html | 36 + .../multi/multi__tracing_bus_events.html | 42 + ...lti__using_the_pluggable_architecture.html | 471 + .../multi/multi__whats_new_in_2_0.html | 33 + ...y_do_you_need_spring_cloud_kubernetes.html | 4 + .../multi__zipkin_stream_span_consumer.html | 5 + .../multi/multi_content-type-management.html | 78 + Greenwich.SR5/multi/multi_contract-dsl.html | 2208 + .../multi/multi_gateway-how-it-works.html | 3 + ..._gateway-request-predicates-factories.html | 133 + .../multi/multi_gateway-starter.html | 11 + .../multi/multi_gradle-add-gradle-plugin.html | 813 + Greenwich.SR5/multi/multi_pr01.html | 11 + .../multi/multi_retrying-failed-requests.html | 27 + .../multi/multi_schema-evolution.html | 88 + .../multi_spring-cloud-consul-agent.html | 3 + .../multi/multi_spring-cloud-consul-bus.html | 3 + .../multi_spring-cloud-consul-config.html | 38 + .../multi_spring-cloud-consul-discovery.html | 109 + .../multi_spring-cloud-consul-hystrix.html | 3 + .../multi_spring-cloud-consul-install.html | 3 + .../multi_spring-cloud-consul-retry.html | 11 + .../multi_spring-cloud-consul-turbine.html | 27 + .../multi_spring-cloud-eureka-server.html | 121 + .../multi/multi_spring-cloud-feign.html | 221 + .../multi/multi_spring-cloud-gcp-core.html | 23 + .../multi_spring-cloud-gcp-reference.html | 3 + .../multi/multi_spring-cloud-ribbon.html | 130 + ..._spring-cloud-stream-overview-binders.html | 76 + ...ing-cloud-stream-overview-introducing.html | 42 + ...cloud-stream-overview-metrics-emitter.html | 50 + .../multi_spring-cloud-zookeeper-config.html | 61 + ...i_spring-cloud-zookeeper-dependencies.html | 86 + ...ng-cloud-zookeeper-dependency-watcher.html | 21 + ...ulti_spring-cloud-zookeeper-discovery.html | 53 + .../multi_spring-cloud-zookeeper-install.html | 40 + .../multi_spring-cloud-zookeeper-netflix.html | 7 + ...ring-cloud-zookeeper-service-registry.html | 26 + Greenwich.SR5/multi/multi_spring-cloud.html | 3 + .../multi_stub-runner-for-messaging.html | 360 + .../multi/multi_troubleshooting.html | 8 + .../multi/multi_vault-lease-renewal.html | 22 + .../multi_vault.config.authentication.html | 199 + ...ulti_vault.config.backends.configurer.html | 22 + ...ult.config.backends.database-backends.html | 103 + .../multi/multi_vault.config.backends.html | 99 + .../multi/multi_vault.config.fail-fast.html | 8 + .../multi/multi_vault.config.ssl.html | 13 + Greenwich.SR5/single/css/highlight.css | 35 + Greenwich.SR5/single/css/manual-multipage.css | 9 + .../single/css/manual-singlepage.css | 6 + Greenwich.SR5/single/css/manual.css | 342 + Greenwich.SR5/single/images/background.png | Bin 0 -> 18255 bytes Greenwich.SR5/single/images/callouts/1.png | Bin 0 -> 329 bytes Greenwich.SR5/single/images/callouts/2.png | Bin 0 -> 353 bytes Greenwich.SR5/single/images/callouts/3.png | Bin 0 -> 350 bytes Greenwich.SR5/single/images/caution.png | Bin 0 -> 2099 bytes Greenwich.SR5/single/images/important.png | Bin 0 -> 2085 bytes Greenwich.SR5/single/images/logo.png | Bin 0 -> 4387 bytes Greenwich.SR5/single/images/note.png | Bin 0 -> 2257 bytes Greenwich.SR5/single/images/tip.png | Bin 0 -> 931 bytes Greenwich.SR5/single/images/warning.png | Bin 0 -> 2130 bytes Greenwich.SR5/single/spring-cloud.html | 17153 +++++++ Greenwich.SR5/spring-cloud.html | 550 + Greenwich.SR5/spring-cloud.xml | 39686 ++++++++++++++++ 273 files changed, 76636 insertions(+) create mode 100644 Greenwich.SR5/css/highlight.css create mode 100644 Greenwich.SR5/css/manual-multipage.css create mode 100644 Greenwich.SR5/css/manual-singlepage.css create mode 100644 Greenwich.SR5/css/manual.css create mode 100644 Greenwich.SR5/ghpages.sh create mode 100644 Greenwich.SR5/images/.gitkeep create mode 100644 Greenwich.SR5/images/Deps.png create mode 100644 Greenwich.SR5/images/Hystrix.png create mode 100644 Greenwich.SR5/images/HystrixFallback.png create mode 100644 Greenwich.SR5/images/HystrixGraph.png create mode 100644 Greenwich.SR5/images/RequestLatency.png create mode 100644 Greenwich.SR5/images/SCSt-groups.png create mode 100644 Greenwich.SR5/images/SCSt-overview.png create mode 100644 Greenwich.SR5/images/SCSt-partitioning.png create mode 100644 Greenwich.SR5/images/SCSt-sensors.png create mode 100644 Greenwich.SR5/images/SCSt-with-binder.png create mode 100644 Greenwich.SR5/images/Stubs1.png create mode 100644 Greenwich.SR5/images/Stubs2.png create mode 100644 Greenwich.SR5/images/background.png create mode 100644 Greenwich.SR5/images/callouts/1.png create mode 100644 Greenwich.SR5/images/callouts/2.png create mode 100644 Greenwich.SR5/images/callouts/3.png create mode 100644 Greenwich.SR5/images/caution.png create mode 100644 Greenwich.SR5/images/custom_vs_global_error_channels.png create mode 100644 Greenwich.SR5/images/dependencies.png create mode 100644 Greenwich.SR5/images/important.png create mode 100644 Greenwich.SR5/images/kibana.png create mode 100644 Greenwich.SR5/images/logo.png create mode 100644 Greenwich.SR5/images/note.png create mode 100644 Greenwich.SR5/images/parents.png create mode 100644 Greenwich.SR5/images/part-bindings.png create mode 100644 Greenwich.SR5/images/part-exchange.png create mode 100644 Greenwich.SR5/images/part-queues.png create mode 100644 Greenwich.SR5/images/producers-consumers.png create mode 100644 Greenwich.SR5/images/pws.png create mode 100644 Greenwich.SR5/images/rabbit-binder.png create mode 100644 Greenwich.SR5/images/redis-binder.png create mode 100644 Greenwich.SR5/images/registration.png create mode 100644 Greenwich.SR5/images/schema_reading.png create mode 100644 Greenwich.SR5/images/schema_resolution.png create mode 100644 Greenwich.SR5/images/spring-cloud-launcher-eureka-dashboard.png create mode 100644 Greenwich.SR5/images/spring-cloud-launcher-log.png create mode 100644 Greenwich.SR5/images/stream-initializr.png create mode 100644 Greenwich.SR5/images/tip.png create mode 100644 Greenwich.SR5/images/trace-id.png create mode 100644 Greenwich.SR5/images/warning.png create mode 100644 Greenwich.SR5/images/zipkin-error-trace-screenshot.png create mode 100644 Greenwich.SR5/images/zipkin-error-traces.png create mode 100644 Greenwich.SR5/images/zipkin-trace-screenshot.png create mode 100644 Greenwich.SR5/images/zipkin-traces.png create mode 100644 Greenwich.SR5/images/zipkin-ui.png create mode 100644 Greenwich.SR5/multi/css/highlight.css create mode 100644 Greenwich.SR5/multi/css/manual-multipage.css create mode 100644 Greenwich.SR5/multi/css/manual-singlepage.css create mode 100644 Greenwich.SR5/multi/css/manual.css create mode 100644 Greenwich.SR5/multi/images/background.png create mode 100644 Greenwich.SR5/multi/images/callouts/1.png create mode 100644 Greenwich.SR5/multi/images/callouts/2.png create mode 100644 Greenwich.SR5/multi/images/callouts/3.png create mode 100644 Greenwich.SR5/multi/images/caution.png create mode 100644 Greenwich.SR5/multi/images/important.png create mode 100644 Greenwich.SR5/multi/images/logo.png create mode 100644 Greenwich.SR5/multi/images/note.png create mode 100644 Greenwich.SR5/multi/images/tip.png create mode 100644 Greenwich.SR5/multi/images/warning.png create mode 100644 Greenwich.SR5/multi/multi__a_brief_history_of_springs_data_integration_journey.html create mode 100644 Greenwich.SR5/multi/multi__actuator_api.html create mode 100644 Greenwich.SR5/multi/multi__additional_resources.html create mode 100644 Greenwich.SR5/multi/multi__addressing_all_instances_of_a_service.html create mode 100644 Greenwich.SR5/multi/multi__addressing_an_instance.html create mode 100644 Greenwich.SR5/multi/multi__apache_kafka_binder.html create mode 100644 Greenwich.SR5/multi/multi__apache_kafka_streams_binder.html create mode 100644 Greenwich.SR5/multi/multi__appendix_compendium_of_configuration_properties.html create mode 100644 Greenwich.SR5/multi/multi__binder_implementations.html create mode 100644 Greenwich.SR5/multi/multi__broadcasting_your_own_events.html create mode 100644 Greenwich.SR5/multi/multi__building.html create mode 100644 Greenwich.SR5/multi/multi__building_a_simple_gateway_using_spring_mvc_or_webflux.html create mode 100644 Greenwich.SR5/multi/multi__building_and_running_a_function.html create mode 100644 Greenwich.SR5/multi/multi__bus_endpoints.html create mode 100644 Greenwich.SR5/multi/multi__circuit_breaker_hystrix_clients.html create mode 100644 Greenwich.SR5/multi/multi__circuit_breaker_hystrix_dashboard.html create mode 100644 Greenwich.SR5/multi/multi__client_side_usage_2.html create mode 100644 Greenwich.SR5/multi/multi__cloud_foundry.html create mode 100644 Greenwich.SR5/multi/multi__cloud_identity_aware_proxy_iap_authentication.html create mode 100644 Greenwich.SR5/multi/multi__cloud_memorystore_for_redis.html create mode 100644 Greenwich.SR5/multi/multi__cloud_native_applications.html create mode 100644 Greenwich.SR5/multi/multi__configuration_2.html create mode 100644 Greenwich.SR5/multi/multi__configuration_options.html create mode 100644 Greenwich.SR5/multi/multi__configuring_authentication_downstream_of_a_zuul_proxy.html create mode 100644 Greenwich.SR5/multi/multi__configuring_route_predicate_factories_and_gateway_filter_factories.html create mode 100644 Greenwich.SR5/multi/multi__contributing.html create mode 100644 Greenwich.SR5/multi/multi__cors_configuration.html create mode 100644 Greenwich.SR5/multi/multi__current_span.html create mode 100644 Greenwich.SR5/multi/multi__current_tracing_component.html create mode 100644 Greenwich.SR5/multi/multi__customization.html create mode 100644 Greenwich.SR5/multi/multi__customizations.html create mode 100644 Greenwich.SR5/multi/multi__customizing_the_message_broker.html create mode 100644 Greenwich.SR5/multi/multi__dependency_management.html create mode 100644 Greenwich.SR5/multi/multi__deploying_a_packaged_function.html create mode 100644 Greenwich.SR5/multi/multi__developer_guide.html create mode 100644 Greenwich.SR5/multi/multi__discovery.html create mode 100644 Greenwich.SR5/multi/multi__discoveryclient_for_kubernetes.html create mode 100644 Greenwich.SR5/multi/multi__dynamic_compilation.html create mode 100644 Greenwich.SR5/multi/multi__embedding_the_config_server.html create mode 100644 Greenwich.SR5/multi/multi__examples_2.html create mode 100644 Greenwich.SR5/multi/multi__external_configuration_archaius.html create mode 100644 Greenwich.SR5/multi/multi__features.html create mode 100644 Greenwich.SR5/multi/multi__features_2.html create mode 100644 Greenwich.SR5/multi/multi__function_catalog_and_flexible_function_signatures.html create mode 100644 Greenwich.SR5/multi/multi__functional_bean_definitions.html create mode 100644 Greenwich.SR5/multi/multi__gatewayfilter_factories.html create mode 100644 Greenwich.SR5/multi/multi__getting_started.html create mode 100644 Greenwich.SR5/multi/multi__getting_started_2.html create mode 100644 Greenwich.SR5/multi/multi__global_filters.html create mode 100644 Greenwich.SR5/multi/multi__glossary.html create mode 100644 Greenwich.SR5/multi/multi__google_cloud_pubsub.html create mode 100644 Greenwich.SR5/multi/multi__google_cloud_vision.html create mode 100644 Greenwich.SR5/multi/multi__health_indicator_5.html create mode 100644 Greenwich.SR5/multi/multi__http_clients.html create mode 100644 Greenwich.SR5/multi/multi__httpheadersfilters.html create mode 100644 Greenwich.SR5/multi/multi__hystrix_timeouts_and_ribbon_clients.html create mode 100644 Greenwich.SR5/multi/multi__instrumentation.html create mode 100644 Greenwich.SR5/multi/multi__integrations.html create mode 100644 Greenwich.SR5/multi/multi__inter_application_communication.html create mode 100644 Greenwich.SR5/multi/multi__introduction.html create mode 100644 Greenwich.SR5/multi/multi__introduction_2.html create mode 100644 Greenwich.SR5/multi/multi__introduction_4.html create mode 100644 Greenwich.SR5/multi/multi__kotlin_support.html create mode 100644 Greenwich.SR5/multi/multi__kubernetes_ecosystem_awareness.html create mode 100644 Greenwich.SR5/multi/multi__kubernetes_native_service_discovery.html create mode 100644 Greenwich.SR5/multi/multi__kubernetes_propertysource_implementations.html create mode 100644 Greenwich.SR5/multi/multi__leader_election.html create mode 100644 Greenwich.SR5/multi/multi__links.html create mode 100644 Greenwich.SR5/multi/multi__main_concepts.html create mode 100644 Greenwich.SR5/multi/multi__managing_spans_with_annotations.html create mode 100644 Greenwich.SR5/multi/multi__migrations.html create mode 100644 Greenwich.SR5/multi/multi__modules_in_maintenance_mode.html create mode 100644 Greenwich.SR5/multi/multi__more_detail.html create mode 100644 Greenwich.SR5/multi/multi__naming_spans.html create mode 100644 Greenwich.SR5/multi/multi__other_resources.html create mode 100644 Greenwich.SR5/multi/multi__pod_health_indicator.html create mode 100644 Greenwich.SR5/multi/multi__polyglot_support_with_sidecar.html create mode 100644 Greenwich.SR5/multi/multi__programming_model.html create mode 100644 Greenwich.SR5/multi/multi__propagation.html create mode 100644 Greenwich.SR5/multi/multi__push_notifications_and_spring_cloud_bus.html create mode 100644 Greenwich.SR5/multi/multi__quick_start.html create mode 100644 Greenwich.SR5/multi/multi__quick_start_2.html create mode 100644 Greenwich.SR5/multi/multi__quick_start_3.html create mode 100644 Greenwich.SR5/multi/multi__quick_start_4.html create mode 100644 Greenwich.SR5/multi/multi__quickstart.html create mode 100644 Greenwich.SR5/multi/multi__rabbitmq_binder.html create mode 100644 Greenwich.SR5/multi/multi__reactor_netty_access_logs.html create mode 100644 Greenwich.SR5/multi/multi__ribbon_discovery_in_kubernetes.html create mode 100644 Greenwich.SR5/multi/multi__router_and_filter_zuul.html create mode 100644 Greenwich.SR5/multi/multi__running_examples.html create mode 100644 Greenwich.SR5/multi/multi__sample_13.html create mode 100644 Greenwich.SR5/multi/multi__samples.html create mode 100644 Greenwich.SR5/multi/multi__sampling.html create mode 100644 Greenwich.SR5/multi/multi__security_configurations_inside_kubernetes.html create mode 100644 Greenwich.SR5/multi/multi__sending_spans_to_zipkin.html create mode 100644 Greenwich.SR5/multi/multi__serverless_platform_adapters.html create mode 100644 Greenwich.SR5/multi/multi__service_discovery_eureka_clients.html create mode 100644 Greenwich.SR5/multi/multi__service_id_must_be_unique.html create mode 100644 Greenwich.SR5/multi/multi__service_registry_configuration.html create mode 100644 Greenwich.SR5/multi/multi__service_registry_implementation.html create mode 100644 Greenwich.SR5/multi/multi__serving_alternative_formats.html create mode 100644 Greenwich.SR5/multi/multi__serving_plain_text.html create mode 100644 Greenwich.SR5/multi/multi__single_sign_on_2.html create mode 100644 Greenwich.SR5/multi/multi__span_lifecycle.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_bus.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_commons_common_abstractions.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_config.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_config_2.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_config_client.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_config_server.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_consul.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_context_application_context_services.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract_2.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract_faq.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract_stub_runner.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_introduction.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_messaging.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_setup.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_contract_wiremock.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_for_cloud_foundry.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_function_2.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_gateway.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_kubernetes.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_netflix.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_openfeign.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_security.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_sleuth.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_sleuth_2.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_stream.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_stream_2.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_vault.html create mode 100644 Greenwich.SR5/multi/multi__spring_cloud_zookeeper.html create mode 100644 Greenwich.SR5/multi/multi__spring_data_cloud_datastore.html create mode 100644 Greenwich.SR5/multi/multi__spring_data_cloud_spanner.html create mode 100644 Greenwich.SR5/multi/multi__spring_integration.html create mode 100644 Greenwich.SR5/multi/multi__spring_jdbc.html create mode 100644 Greenwich.SR5/multi/multi__spring_resources.html create mode 100644 Greenwich.SR5/multi/multi__stackdriver_logging.html create mode 100644 Greenwich.SR5/multi/multi__standalone_streaming_applications.html create mode 100644 Greenwich.SR5/multi/multi__standalone_web_applications.html create mode 100644 Greenwich.SR5/multi/multi__starters.html create mode 100644 Greenwich.SR5/multi/multi__testing.html create mode 100644 Greenwich.SR5/multi/multi__tls_ssl.html create mode 100644 Greenwich.SR5/multi/multi__tracing_bus_events.html create mode 100644 Greenwich.SR5/multi/multi__using_the_pluggable_architecture.html create mode 100644 Greenwich.SR5/multi/multi__whats_new_in_2_0.html create mode 100644 Greenwich.SR5/multi/multi__why_do_you_need_spring_cloud_kubernetes.html create mode 100644 Greenwich.SR5/multi/multi__zipkin_stream_span_consumer.html create mode 100644 Greenwich.SR5/multi/multi_content-type-management.html create mode 100644 Greenwich.SR5/multi/multi_contract-dsl.html create mode 100644 Greenwich.SR5/multi/multi_gateway-how-it-works.html create mode 100644 Greenwich.SR5/multi/multi_gateway-request-predicates-factories.html create mode 100644 Greenwich.SR5/multi/multi_gateway-starter.html create mode 100644 Greenwich.SR5/multi/multi_gradle-add-gradle-plugin.html create mode 100644 Greenwich.SR5/multi/multi_pr01.html create mode 100644 Greenwich.SR5/multi/multi_retrying-failed-requests.html create mode 100644 Greenwich.SR5/multi/multi_schema-evolution.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-agent.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-bus.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-config.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-discovery.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-hystrix.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-install.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-retry.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-consul-turbine.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-eureka-server.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-feign.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-gcp-core.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-gcp-reference.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-ribbon.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-stream-overview-binders.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-stream-overview-introducing.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-stream-overview-metrics-emitter.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-zookeeper-config.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependencies.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependency-watcher.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-zookeeper-discovery.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-zookeeper-install.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-zookeeper-netflix.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud-zookeeper-service-registry.html create mode 100644 Greenwich.SR5/multi/multi_spring-cloud.html create mode 100644 Greenwich.SR5/multi/multi_stub-runner-for-messaging.html create mode 100644 Greenwich.SR5/multi/multi_troubleshooting.html create mode 100644 Greenwich.SR5/multi/multi_vault-lease-renewal.html create mode 100644 Greenwich.SR5/multi/multi_vault.config.authentication.html create mode 100644 Greenwich.SR5/multi/multi_vault.config.backends.configurer.html create mode 100644 Greenwich.SR5/multi/multi_vault.config.backends.database-backends.html create mode 100644 Greenwich.SR5/multi/multi_vault.config.backends.html create mode 100644 Greenwich.SR5/multi/multi_vault.config.fail-fast.html create mode 100644 Greenwich.SR5/multi/multi_vault.config.ssl.html create mode 100644 Greenwich.SR5/single/css/highlight.css create mode 100644 Greenwich.SR5/single/css/manual-multipage.css create mode 100644 Greenwich.SR5/single/css/manual-singlepage.css create mode 100644 Greenwich.SR5/single/css/manual.css create mode 100644 Greenwich.SR5/single/images/background.png create mode 100644 Greenwich.SR5/single/images/callouts/1.png create mode 100644 Greenwich.SR5/single/images/callouts/2.png create mode 100644 Greenwich.SR5/single/images/callouts/3.png create mode 100644 Greenwich.SR5/single/images/caution.png create mode 100644 Greenwich.SR5/single/images/important.png create mode 100644 Greenwich.SR5/single/images/logo.png create mode 100644 Greenwich.SR5/single/images/note.png create mode 100644 Greenwich.SR5/single/images/tip.png create mode 100644 Greenwich.SR5/single/images/warning.png create mode 100644 Greenwich.SR5/single/spring-cloud.html create mode 100644 Greenwich.SR5/spring-cloud.html create mode 100644 Greenwich.SR5/spring-cloud.xml diff --git a/Greenwich.SR5/css/highlight.css b/Greenwich.SR5/css/highlight.css new file mode 100644 index 00000000..3850f8b9 --- /dev/null +++ b/Greenwich.SR5/css/highlight.css @@ -0,0 +1,35 @@ +/* + code highlight CSS resemblign the Eclipse IDE default color schema + @author Costin Leau +*/ + +.hl-keyword { + color: #7F0055; + font-weight: bold; +} + +.hl-comment { + color: #3F5F5F; + font-style: italic; +} + +.hl-multiline-comment { + color: #3F5FBF; + font-style: italic; +} + +.hl-tag { + color: #3F7F7F; +} + +.hl-attribute { + color: #7F007F; +} + +.hl-value { + color: #2A00FF; +} + +.hl-string { + color: #2A00FF; +} \ No newline at end of file diff --git a/Greenwich.SR5/css/manual-multipage.css b/Greenwich.SR5/css/manual-multipage.css new file mode 100644 index 00000000..b790654b --- /dev/null +++ b/Greenwich.SR5/css/manual-multipage.css @@ -0,0 +1,9 @@ +@IMPORT url("manual.css"); + +body.firstpage { + background: url("../images/background.png") no-repeat center top; +} + +div.part h1 { + border-top: none; +} diff --git a/Greenwich.SR5/css/manual-singlepage.css b/Greenwich.SR5/css/manual-singlepage.css new file mode 100644 index 00000000..303192a8 --- /dev/null +++ b/Greenwich.SR5/css/manual-singlepage.css @@ -0,0 +1,6 @@ +@IMPORT url("manual.css"); + +body { + background: url("../images/background.png") no-repeat center top; +} + diff --git a/Greenwich.SR5/css/manual.css b/Greenwich.SR5/css/manual.css new file mode 100644 index 00000000..20cf07da --- /dev/null +++ b/Greenwich.SR5/css/manual.css @@ -0,0 +1,342 @@ +@IMPORT url("highlight.css"); + +html { + padding: 0pt; + margin: 0pt; +} + +body { + color: #333333; + margin: 15px 30px; + font-family: Helvetica, Arial, Freesans, Clean, Sans-serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +code { + font-size: 16px; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +:not(a) > code { + color: #6D180B; +} + +:not(pre) > code { + background-color: #F2F2F2; + border: 1px solid #CCCCCC; + border-radius: 4px; + padding: 1px 3px 0; + text-shadow: none; + white-space: nowrap; +} + +body > *:first-child { + margin-top: 0 !important; +} + +div { + margin: 0pt; +} + +hr { + border: 1px solid #CCCCCC; + background: #CCCCCC; +} + +h1, h2, h3, h4, h5, h6 { + color: #000000; + cursor: text; + font-weight: bold; + margin: 30px 0 10px; + padding: 0; +} + +h1, h2, h3 { + margin: 40px 0 10px; +} + +h1 { + margin: 70px 0 30px; + padding-top: 20px; +} + +div.part h1 { + border-top: 1px dotted #CCCCCC; +} + +h1, h1 code { + font-size: 32px; +} + +h2, h2 code { + font-size: 24px; +} + +h3, h3 code { + font-size: 20px; +} + +h4, h1 code, h5, h5 code, h6, h6 code { + font-size: 18px; +} + +div.book, div.chapter, div.appendix, div.part, div.preface { + min-width: 300px; + max-width: 1200px; + margin: 0 auto; +} + +p.releaseinfo { + font-weight: bold; + margin-bottom: 40px; + margin-top: 40px; +} + +div.authorgroup { + line-height: 1; +} + +p.copyright { + line-height: 1; + margin-bottom: -5px; +} + +.legalnotice p { + font-style: italic; + font-size: 14px; + line-height: 1; +} + +div.titlepage + p, div.titlepage + p { + margin-top: 0; +} + +pre { + line-height: 1.0; + color: black; +} + +a { + color: #4183C4; + text-decoration: none; +} + +p { + margin: 15px 0; + text-align: left; +} + +ul, ol { + padding-left: 30px; +} + +li p { + margin: 0; +} + +div.table { + margin: 1em; + padding: 0.5em; + text-align: center; +} + +div.table table, div.informaltable table { + display: table; + width: 100%; +} + +div.table td { + padding-left: 7px; + padding-right: 7px; +} + +.sidebar { + line-height: 1.4; + padding: 0 20px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; +} + +.sidebar p.title { + color: #6D180B; +} + +pre.programlisting, pre.screen { + font-size: 15px; + padding: 6px 10px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; + clear: both; + overflow: auto; + line-height: 1.4; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +table { + border-collapse: collapse; + border-spacing: 0; + border: 1px solid #DDDDDD !important; + border-radius: 4px !important; + border-collapse: separate !important; + line-height: 1.6; +} + +table thead { + background: #F5F5F5; +} + +table tr { + border: none; + border-bottom: none; +} + +table th { + font-weight: bold; +} + +table th, table td { + border: none !important; + padding: 6px 13px; +} + +table tr:nth-child(2n) { + background-color: #F8F8F8; +} + +td p { + margin: 0 0 15px 0; +} + +div.table-contents td p { + margin: 0; +} + +div.important *, div.note *, div.tip *, div.warning *, div.navheader *, div.navfooter *, div.calloutlist * { + border: none !important; + background: none !important; + margin: 0; +} + +div.important p, div.note p, div.tip p, div.warning p { + color: #6F6F6F; + line-height: 1.6; +} + +div.important code, div.note code, div.tip code, div.warning code { + background-color: #F2F2F2 !important; + border: 1px solid #CCCCCC !important; + border-radius: 4px !important; + padding: 1px 3px 0 !important; + text-shadow: none !important; + white-space: nowrap !important; +} + +.note th, .tip th, .warning th { + display: none; +} + +.note tr:first-child td, .tip tr:first-child td, .warning tr:first-child td { + border-right: 1px solid #CCCCCC !important; + padding-top: 10px; +} + +div.calloutlist p, div.calloutlist td { + padding: 0; + margin: 0; +} + +div.calloutlist > table > tbody > tr > td:first-child { + padding-left: 10px; + width: 30px !important; +} + +div.important, div.note, div.tip, div.warning { + margin-left: 0px !important; + margin-right: 20px !important; + margin-top: 20px; + margin-bottom: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +div.toc { + line-height: 1.2; +} + +dl, dt { + margin-top: 1px; + margin-bottom: 0; +} + +div.toc > dl > dt { + font-size: 32px; + font-weight: bold; + margin: 30px 0 10px 0; + display: block; +} + +div.toc > dl > dd > dl > dt { + font-size: 24px; + font-weight: bold; + margin: 20px 0 10px 0; + display: block; +} + +div.toc > dl > dd > dl > dd > dl > dt { + font-weight: bold; + font-size: 20px; + margin: 10px 0 0 0; +} + +tbody.footnotes * { + border: none !important; +} + +div.footnote p { + margin: 0; + line-height: 1; +} + +div.footnote p sup { + margin-right: 6px; + vertical-align: middle; +} + +div.navheader { + border-bottom: 1px solid #CCCCCC; +} + +div.navfooter { + border-top: 1px solid #CCCCCC; +} + +.title { + margin-left: -1em; + padding-left: 1em; +} + +.title > a { + position: absolute; + visibility: hidden; + display: block; + font-size: 0.85em; + margin-top: 0.05em; + margin-left: -1em; + vertical-align: text-top; + color: black; +} + +.title > a:before { + content: "\00A7"; +} + +.title:hover > a, .title > a:hover, .title:hover > a:hover { + visibility: visible; +} + +.title:focus > a, .title > a:focus, .title:focus > a:focus { + outline: 0; +} diff --git a/Greenwich.SR5/ghpages.sh b/Greenwich.SR5/ghpages.sh new file mode 100644 index 00000000..e0e69be3 --- /dev/null +++ b/Greenwich.SR5/ghpages.sh @@ -0,0 +1,348 @@ +#!/bin/bash -x + +set -e + +# Set default props like MAVEN_PATH, ROOT_FOLDER etc. +function set_default_props() { + # The script should be executed from the root folder + ROOT_FOLDER=`pwd` + echo "Current folder is ${ROOT_FOLDER}" + + if [[ ! -e "${ROOT_FOLDER}/.git" ]]; then + echo "You're not in the root folder of the project!" + exit 1 + fi + + # Prop that will let commit the changes + COMMIT_CHANGES="no" + MAVEN_PATH=${MAVEN_PATH:-} + echo "Path to Maven is [${MAVEN_PATH}]" + REPO_NAME=${PWD##*/} + echo "Repo name is [${REPO_NAME}]" + SPRING_CLOUD_STATIC_REPO=${SPRING_CLOUD_STATIC_REPO:-git@github.com:spring-cloud/spring-cloud-static.git} + echo "Spring Cloud Static repo is [${SPRING_CLOUD_STATIC_REPO}" +} + +# Check if gh-pages exists and docs have been built +function check_if_anything_to_sync() { + git remote set-url --push origin `git config remote.origin.url | sed -e 's/^git:/https:/'` + + if ! (git remote set-branches --add origin gh-pages && git fetch -q) && [[ "${RELEASE_TRAIN}" != "yes" ]] ; then + echo "No gh-pages, so not syncing" + exit 0 + fi + + if ! [ -d docs/target/generated-docs ] && ! [ "${BUILD}" == "yes" ]; then + echo "No gh-pages sources in docs/target/generated-docs, so not syncing" + exit 0 + fi +} + +function retrieve_current_branch() { + # Code getting the name of the current branch. For master we want to publish as we did until now + # http://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch + # If there is a branch already passed will reuse it - otherwise will try to find it + CURRENT_BRANCH=${BRANCH} + if [[ -z "${CURRENT_BRANCH}" ]] ; then + CURRENT_BRANCH=$(git symbolic-ref -q HEAD) + CURRENT_BRANCH=${CURRENT_BRANCH##refs/heads/} + CURRENT_BRANCH=${CURRENT_BRANCH:-HEAD} + fi + echo "Current branch is [${CURRENT_BRANCH}]" + git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script" +} + +# Switches to the provided value of the release version. We always prefix it with `v` +function switch_to_tag() { + if [[ "${RELEASE_TRAIN}" != "yes" ]] ; then + git checkout v${VERSION} + fi +} + +# Build the docs if switch is on +function build_docs_if_applicable() { + if [[ "${BUILD}" == "yes" ]] ; then + ./mvnw clean install -P docs -pl docs -DskipTests + fi +} + +# Get the name of the `docs.main` property +# Get whitelisted branches - assumes that a `docs` module is available under `docs` profile +function retrieve_doc_properties() { + MAIN_ADOC_VALUE=$("${MAVEN_PATH}"mvn -q \ + -Dexec.executable="echo" \ + -Dexec.args='${docs.main}' \ + org.codehaus.mojo:exec-maven-plugin:1.3.1:exec \ + -P docs \ + -pl docs) + echo "Extracted 'main.adoc' from Maven build [${MAIN_ADOC_VALUE}]" + + + WHITELIST_PROPERTY=${WHITELIST_PROPERTY:-"docs.whitelisted.branches"} + WHITELISTED_BRANCHES_VALUE=$("${MAVEN_PATH}"mvn -q \ + -Dexec.executable="echo" \ + -Dexec.args="\${${WHITELIST_PROPERTY}}" \ + org.codehaus.mojo:exec-maven-plugin:1.3.1:exec \ + -P docs \ + -pl docs) + echo "Extracted '${WHITELIST_PROPERTY}' from Maven build [${WHITELISTED_BRANCHES_VALUE}]" +} + +# Stash any outstanding changes +function stash_changes() { + git diff-index --quiet HEAD && dirty=$? || (echo "Failed to check if the current repo is dirty. Assuming that it is." && dirty="1") + if [ "$dirty" != "0" ]; then git stash; fi +} + +# Switch to gh-pages branch to sync it with current branch +function add_docs_from_target() { + local DESTINATION_REPO_FOLDER + if [[ -z "${DESTINATION}" && -z "${CLONE}" ]] ; then + DESTINATION_REPO_FOLDER=${ROOT_FOLDER} + elif [[ "${CLONE}" == "yes" ]]; then + mkdir -p ${ROOT_FOLDER}/target + local clonedStatic=${ROOT_FOLDER}/target/spring-cloud-static + if [[ ! -e "${clonedStatic}/.git" ]]; then + echo "Cloning Spring Cloud Static to target" + git clone ${SPRING_CLOUD_STATIC_REPO} ${clonedStatic} && cd ${clonedStatic} && git checkout gh-pages + else + echo "Spring Cloud Static already cloned - will pull changes" + cd ${clonedStatic} && git checkout gh-pages && git pull origin gh-pages + fi + if [[ -z "${RELEASE_TRAIN}" ]] ; then + DESTINATION_REPO_FOLDER=${clonedStatic}/${REPO_NAME} + else + DESTINATION_REPO_FOLDER=${clonedStatic} + fi + mkdir -p ${DESTINATION_REPO_FOLDER} + else + if [[ ! -e "${DESTINATION}/.git" ]]; then + echo "[${DESTINATION}] is not a git repository" + exit 1 + fi + if [[ -z "${RELEASE_TRAIN}" ]] ; then + DESTINATION_REPO_FOLDER=${DESTINATION}/${REPO_NAME} + else + DESTINATION_REPO_FOLDER=${DESTINATION} + fi + mkdir -p ${DESTINATION_REPO_FOLDER} + echo "Destination was provided [${DESTINATION}]" + fi + cd ${DESTINATION_REPO_FOLDER} + git checkout gh-pages + git pull origin gh-pages + + # Add git branches + ################################################################### + if [[ -z "${VERSION}" && -z "${RELEASE_TRAIN}" ]] ; then + copy_docs_for_current_version + else + copy_docs_for_provided_version + fi + commit_changes_if_applicable +} + + +# Copies the docs by using the retrieved properties from Maven build +function copy_docs_for_current_version() { + if [[ "${CURRENT_BRANCH}" == "master" ]] ; then + echo -e "Current branch is master - will copy the current docs only to the root folder" + for f in docs/target/generated-docs/*; do + file=${f#docs/target/generated-docs/*} + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then + # Not ignored... + cp -rf $f ${ROOT_FOLDER}/ + git add -A ${ROOT_FOLDER}/$file + fi + done + COMMIT_CHANGES="yes" + else + echo -e "Current branch is [${CURRENT_BRANCH}]" + # http://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin + if [[ ",${WHITELISTED_BRANCHES_VALUE}," = *",${CURRENT_BRANCH},"* ]] ; then + mkdir -p ${ROOT_FOLDER}/${CURRENT_BRANCH} + echo -e "Branch [${CURRENT_BRANCH}] is whitelisted! Will copy the current docs to the [${CURRENT_BRANCH}] folder" + for f in docs/target/generated-docs/*; do + file=${f#docs/target/generated-docs/*} + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then + # Not ignored... + # We want users to access 1.0.0.RELEASE/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html + if [[ "${file}" == "${MAIN_ADOC_VALUE}.html" ]] ; then + # We don't want to copy the spring-cloud-sleuth.html + # we want it to be converted to index.html + cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html + git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/index.html + else + cp -rf $f ${ROOT_FOLDER}/${CURRENT_BRANCH} + git add -A ${ROOT_FOLDER}/${CURRENT_BRANCH}/$file + fi + fi + done + COMMIT_CHANGES="yes" + else + echo -e "Branch [${CURRENT_BRANCH}] is not on the white list! Check out the Maven [${WHITELIST_PROPERTY}] property in + [docs] module available under [docs] profile. Won't commit any changes to gh-pages for this branch." + fi + fi +} + +# Copies the docs by using the explicitly provided version +function copy_docs_for_provided_version() { + local FOLDER=${DESTINATION_REPO_FOLDER}/${VERSION} + mkdir -p ${FOLDER} + echo -e "Current tag is [v${VERSION}] Will copy the current docs to the [${FOLDER}] folder" + for f in ${ROOT_FOLDER}/docs/target/generated-docs/*; do + file=${f#${ROOT_FOLDER}/docs/target/generated-docs/*} + copy_docs_for_branch ${file} ${FOLDER} + done + COMMIT_CHANGES="yes" + CURRENT_BRANCH="v${VERSION}" +} + +# Copies the docs from target to the provided destination +# Params: +# $1 - file from target +# $2 - destination to which copy the files +function copy_docs_for_branch() { + local file=$1 + local destination=$2 + if ! git ls-files -i -o --exclude-standard --directory | grep -q ^${file}$; then + # Not ignored... + # We want users to access 1.0.0.RELEASE/ instead of 1.0.0.RELEASE/spring-cloud.sleuth.html + if [[ ("${file}" == "${MAIN_ADOC_VALUE}.html") || ("${file}" == "${REPO_NAME}.html") ]] ; then + # We don't want to copy the spring-cloud-sleuth.html + # we want it to be converted to index.html + cp -rf $f ${destination}/index.html + git add -A ${destination}/index.html + else + cp -rf $f ${destination} + git add -A ${destination}/$file + fi + fi +} + +function commit_changes_if_applicable() { + if [[ "${COMMIT_CHANGES}" == "yes" ]] ; then + COMMIT_SUCCESSFUL="no" + git commit -a -m "Sync docs from ${CURRENT_BRANCH} to gh-pages" && COMMIT_SUCCESSFUL="yes" || echo "Failed to commit changes" + + # Uncomment the following push if you want to auto push to + # the gh-pages branch whenever you commit to master locally. + # This is a little extreme. Use with care! + ################################################################### + if [[ "${COMMIT_SUCCESSFUL}" == "yes" ]] ; then + git push origin gh-pages + fi + fi +} + +# Switch back to the previous branch and exit block +function checkout_previous_branch() { + # If -version was provided we need to come back to root project + cd ${ROOT_FOLDER} + git checkout ${CURRENT_BRANCH} || echo "Failed to check the branch... continuing with the script" + if [ "$dirty" != "0" ]; then git stash pop; fi + exit 0 +} + +# Assert if properties have been properly passed +function assert_properties() { +echo "VERSION [${VERSION}], RELEASE_TRAIN [${RELEASE_TRAIN}], DESTINATION [${DESTINATION}], CLONE [${CLONE}]" +if [[ "${VERSION}" != "" && (-z "${DESTINATION}" && -z "${CLONE}") ]] ; then echo "Version was set but destination / clone was not!"; exit 1;fi +if [[ ("${DESTINATION}" != "" && "${CLONE}" != "") && -z "${VERSION}" ]] ; then echo "Destination / clone was set but version was not!"; exit 1;fi +if [[ "${DESTINATION}" != "" && "${CLONE}" == "yes" ]] ; then echo "Destination and clone was set. Pick one!"; exit 1;fi +if [[ "${RELEASE_TRAIN}" != "" && -z "${VERSION}" ]] ; then echo "Release train was set but no version was passed!"; exit 1;fi +} + +# Prints the usage +function print_usage() { +cat </` +- if the destination switch is passed (-d) then the script will check if the provided dir is a git repo and then will + switch to gh-pages of that repo and copy the generated docs to `docs//` +- if the release train switch is passed (-r) then the script will check if the provided dir is a git repo and then will + switch to gh-pages of that repo and copy the generated docs to `docs/` + +USAGE: + +You can use the following options: + +-v|--version - the script will apply the whole procedure for a particular library version +-r|--releasetrain - instead of nesting the docs under the project_name/version folder the docs will end up in version +-d|--destination - the root of destination folder where the docs should be copied. You have to use the full path. + E.g. point to spring-cloud-static folder. Can't be used with (-c) +-b|--build - will run the standard build process after checking out the branch +-c|--clone - will automatically clone the spring-cloud-static repo instead of providing the destination. + Obviously can't be used with (-d) + +EOF +} + + +# ========================================== +# ____ ____ _____ _____ _____ _______ +# / ____|/ ____| __ \|_ _| __ \__ __| +# | (___ | | | |__) | | | | |__) | | | +# \___ \| | | _ / | | | ___/ | | +# ____) | |____| | \ \ _| |_| | | | +# |_____/ \_____|_| \_\_____|_| |_| +# +# ========================================== + +while [[ $# > 0 ]] +do +key="$1" +case ${key} in + -v|--version) + VERSION="$2" + shift # past argument + ;; + -r|--releasetrain) + RELEASE_TRAIN="yes" + ;; + -d|--destination) + DESTINATION="$2" + shift # past argument + ;; + -b|--build) + BUILD="yes" + ;; + -c|--clone) + CLONE="yes" + ;; + -h|--help) + print_usage + exit 0 + ;; + *) + echo "Invalid option: [$1]" + print_usage + exit 1 + ;; +esac +shift # past argument or value +done + +assert_properties +set_default_props +check_if_anything_to_sync +if [[ -z "${VERSION}" ]] ; then + retrieve_current_branch +else + switch_to_tag +fi +build_docs_if_applicable +retrieve_doc_properties +stash_changes +add_docs_from_target +checkout_previous_branch diff --git a/Greenwich.SR5/images/.gitkeep b/Greenwich.SR5/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/Greenwich.SR5/images/Deps.png b/Greenwich.SR5/images/Deps.png new file mode 100644 index 0000000000000000000000000000000000000000..1426814308101b8ccf6ef95fd195e37c0a180392 GIT binary patch literal 37192 zcmX_H1yq#Z(^hGuLqJlxq(i#9q`MoGT3~?{kq~fcLAo31X6X)TK{^z07l{R=`+MA3b`Uh=~q7Q9T!Mee~$V zBNcfWJ-?+ra|}Pdfpco2&r`DQAiJejp$UI3UPp#Z-0AXZ0O8 zB>t02ry+wnaMpYtZnT}nc${+#?>MYmWX!OuZ(9UUoUh`47vgqDw1eOiSz@vTVFcFf z(6Z8k4H{AuJ$1vQ251cXn&D9ylvbZg-&1ygf|N{;@vE|qr^Dnh6AZ#*>E*I{`wR8lqa^$AE4Om&&#&+ZIYlY`t8Lw5ztWfPMxC+ z=)GR1j_1S{PD^6$MmuzdJ+}NpZ2-)USH4hglLfVBFD~6K-{2w@)FUxSNO6ChFxpsB z3SDA1Dc`Q9HyFSsrPOP8beY6=YhdY#IlDpROB|H?84tW6?bGwtn|`ez;(|~3MpjQp z!Io4`PrTfw%Xx{8GeserfUOr>Sv$9oq{u}@yDW`lzABp=~+w`IiMc0?%0nG*Ajle;7l*g4H~KXz0i%C(^galf1D0 zF7T0{MM|A8%8*xIfs*tKtBL$0dRYY@T{;@U0R+FU_XMRZ@$;%dKWjFq8G`~Wqo{Pw z1iIU%tSw~%9V8|dc2J%Et1|wjX|%}cJbGhN7pfLTS^{@=TP5UB=(E;YnNox9>=(8a ze4;TuxrGLBa2QDCe@z zme){HTqbeOw(tKMYWjVO=8(l0LQSX|RPAN>yW>m;*Cz|0WJj+Q-rW=?&)=_`OJW!fb6nSR^b=xJ;Mz9kvSYlZRve9Fk z_WG;|WM=58zt*-?c(tzCJ0z`GRR*Pyb^Q6>&~us~?`EK=)T50=7~ep)puY&=z=j^k zJg{w9lCGnE`LzKNs0)d`l7`@M z>>F2W*mOJ&Ao5j!#1q~a4?H7H9OJY^UDu>1ctq^o3`L9^-})1NHiX(h)rBPrHCBF3 z7LngKtI++qkL1UC^{0^8uewrn^znCV<_P1giNm)-^~3`gLc}7dob%DV_0~a@Henc2 zb(q7-=c`Nu!#PRQqLu1~HgE+NmX^>UYi8>k9_Q8rYnCy4rj8L zfR0CY?MRXUg>BbLi98*WO3s0pz%QR53R)lH#Z!HGJsMc%X0r*`)uylC3R7_F#I_@9yU~K0 z;43FoM=}|ds+g4JGp`5qK3xKpLxsbtNHs{%R8_PM(_q=ANoO}{QH_X}Y))$(!{Vmz z*{=^V)P`wO6}f5G-_w_T2zgx?8>Ggpzp0{wb88~C0HVy}V_dO+tCx_*S~<)dEg*_L zWl0)vnMTJKiq76dk@iykIlIu8{9O|^=9_X_q|5-V{aJCQw#{iml@fMb1K-AaY9gb< zapV|Xj>wCv#Z;axj;h^?h<6nIOUEf(A6ZT3O?oA))0e(d;e|DSFv-0f_VIxa4o%Y*_WUB~&_V zjWu1oH#F>tZ+13cLx~Fqn~kJT>KWrsk^P%@*c?t8pKCtrW%Y#)Yhu4^;Yp5_J;9@& zy|XevlGwnQmYLW@ySNnO|JKbspLs+aM(xxQdWNZz@1($X)j<|U(aLFeGI(16?ziOi ze=DMgDZIqbBQm;b6;G0=`5IDeIk$NCPNCAg#fjP=vhU+CPSvx3$kgmwxI zqAf*P%?n+AHi3wCxHwNLkV*h@S`O+?R}dMJP7^Y}MD;&Ig)^;EsZ~+Z$A)KPAg&tY zjkgTT9;3tEi!W$u=!^2_h1lq$)5QfCs)t@|Iql1=PlFWWblz~$DSeM)LwG}YD<#TN zA#lP!@#WQ5EF(rjGWWDZ!^dePE@da`E{#8{Ojw>|9Y_h8q@64%YuYhSx#Nsm`V<@u zQmMwyW^?W2$QKYcYv0LP_=(HNg6u0LoV+I_{pHykc%!M)Z$vwVSfh@*DWP_RbQI59 z!#$Km(EW0ef2Dzo^VAb zdWvoK`(|EOdRQIVD;eqYp|+I1Z@lD=W0}``KQAT?KJ~G&sm`H-JvJ2H<8dhEwn$Hk zl0#gy&hl@Mlh3?=)zEW_zp`K2PW8Pt_U2BRf{J!#N})WE5EnZz3OJ)~mhWx9tSAt_pbr;n+cjbG*a?FPv` z^GH@edPr3}Uvyw318oDW-Yoi;41S31lEmA9@m2!pxlqEfh}G!%4aN8`g<0V`b^Hz` zK$_v6i4EKEQO`3^KQzI3;{dIswlw%e#kCKOY7Pf%QRR5& zI*z~fE4wQKkDxF)%Dm1tVBpgnbz`{6XHzSL-&U-_8hLf!lFV2JE`n4(bGj>Nea?TM z#P{v82$Mq^gdhP$*vH4W>olaR;n}f-zpE6c|79!>Lg0Wf7~(buq58=-E9+a*zP@Se zqOvT`y-%lC2#aB%JTJ1ICiL=36UQcSrp3xDRB4OudK`G2)_hg{r*n?FIp+KAqLEZm zBl%et5Si7qH%!=&#>yZBIf(45)>&{Fx49DZ=sy0bF|V%%v+KUI;3T_iE{s7PM0WfM zkGwtq%Pi;By>j|kVV@@r_wbK^U}S?uCVYYyUS*>zh{vVoc}$np)8EjI6d`pYxQ`Z5;kJW0;=XZ=rk{z9qnf6GYXWd_twYHh_i(Gf# zkobT0Rqvov2qm|V{BiqrRvr3j=oA68b3`6?#@%Tg!2`@g=l85NgFouJ{q9L;po-zRRir4a{E@?Rl@P{oAdB9}A!Hdiw3mtX!G0 z0>K;X2*at2Hr$OJQ{BG|R}ABnhuDItPMoR}g=UJqI%a+{S%2DhJ#R&JclWa&gSkps z2VOjzEw%DMEn#Pze_8J6*k_$C<}?@;^UPCaOC9RVGUNk(y3PN*bUDr~BRN-!rJoaz zJ3mvp@AX)+xg&aCmoCb0(YIDSa@*(GSMNgV6`aQi*}gV@7yC?4Uo>qlF@bw|DELCu z!w*dYPC4)sIjDL}5nvD#;SuU2$fYT2S{lE&chkys5$?BH<2Qe`#u12&#SRoFC2VK4 zcG7Aq(TxjEkoxmn+yjGy0l%}n6YjC!6P~zp?gISkz?&?Y%T{5jlmz2@`H4{a>$f}O zB!7Lm>WDamJ*HLGt}C{1>&<>1cS2i>i zRO=EBT0d)$rp!aNHJ&yx@oGNtyY_R%$Pm!1-|o^HubC9&)(z*6XBd=jBlP zV%Vq<(LYnCK+oixcFhp#FcOtTFW!sM@m`Pj!Wt1|W@9kAF`*zL`kV9OMpoQ<&6U38 zwa&nwsi~=gosK(GJ-39;pXzO;>i7IEZ}g@&v0gsiIhi768EXnHR?6NOoCWFN7{V(r z3j#1Wvq@Q#-E(o)77G5P_-&*A{Irw3@n*4}z3SKNr>g2obDGYRlA-v4YZ{i2@E~r$ z-Hs;)&HAxhXlT@=Y=4rgah~kA)e&zo=kBnkDnZ53)jw3JDxeUzk6 z@F0)29a2pxud%;dsvc63mw)GGS3F2P-VzVm@dwaog~QJn!NrQ%%kfY_mD1JY7gJ$ITw6et!ww4zc9k_I!gok7>r4y z*8{5c8{u0fdwb&?@;y5)#=Oop&aOftmC?uIWsHV8p+eQ{$N9MA{ANCR+qd0ZWwY=H3WmwjO4@Rpvuq+qqx+oqMlQg!eZANSC)d@b=O`gZUf*5Y&Qjq5o zJXULX*CE3&@rExt0tJV2I4s|-p|Zx;sb?xLve^p+IM)ia(V6FEWjj9Z6p#7LJ3H(V z2v03Cw=17dtwTYjlee5L;*{!6!4+~pcILSUucKMD^ZVHoSOi9fnf+YGc$&iB7x9}d zBw44?CrKu`vnnLAedDQ|564f$SEsQmzUiS>i|hu^%AKxOF5j7fa{w^>=M8oh<*3pr z#!lbQ7j@Yd9MM@se`;!K4zi8zmswg$e0EmDk9ljwmlNc;N)J1}r`tYVySn3TpN#7~ z8lT@0xr}4g*G;Qm=g*UHB$1j^@s#6cgX&KwdX}?!?Yp_uHJE|<$#2Wu3`sGU_${0# zb0-V8>lC5Oio4NN(V2M?MJH9BhuG#E;y-AT_yn>s_f<;Q8}>(AN}`%+{53uQD4oJA z^|ui5AiU?YnI^t8HB&?^@;M3h%oL1rIbsFjtZa!-;!81@>H>sM6T`JRU+YZD^Yn#a z=EAqb*Houybm6MFhn*IcIv89u8+R|guHlXgxBkUKDJg9wsMbR zUOg3%U_tcNyM#ocYEBUk|FIo)lw~<&wlTYUxyMd1z5+Qy#o%jZ(%Bc2V+ga&NRv@ittlTm;?9haOHQ*SPO_<J zcCfPy3fAib!}`R4m}_r|CngD2L)4McX{LF60VM=ulzdqJV@3Lr1Qz(!p}lUHwzH2l zqWA1%4+*wwdkCz@G zGMV9zdHjn%DvPQ6EZdy?OES5UtrkHqiAsG2w#Y4#a_w*@?s{9rfb$^cyw2RlIp0jK4LV zy{pu+ly!Kz6ucFZCy`*-fFT+jmnZRYe`%&y0n(jwW&-)TR$2e%)%3^|%|P3`0Ra}? z%i4w#D~eSxo}*Sl2}9i{l(USXcMeLBq_VxnAKN9+t6AoAZ&M^Po4xO*tu-5k%6Fot zs8<2{##fj;`gi}^paX7;_6v5o!W!iIX;6T}LCi@5snMW(veiYiOL3E7Nm*^74}#%} zwIqY)yNx$>*f!1EEjj}p5CKyILr;@Oufmcz0&pyrzMJ8|_~{ zYkfDLt!4bef-h9KmvJ%MR$byF74?jxNoKpaLLNC}#2F;s>SdbKuy()|S}aw}tZeyw zBbZ5*{Du3=1EWo{(SqUEwp1H0;+il^7khZ+he=kZW`>!gcHTDDn2$g7a;=HE^4hpg zFHN{qVj|>?pJB>xXIxBJy|-x>JGoT*tjv8#kZj_#A;3_ zy`9_2u8K-Qil4d#oY#6TW8^$*ZTT{MdqJm_9S@@LbetBu-*=s=Sbbg)ORDzOh@CI? zM9qsCd?+?GgUnVC1=08a9v&W!sV&EA|1#ZygFAj5cXcO66(AE_`1}-^nlcTMeE%o5 z`ODd!U;{>4^|j@!BHKjIQm77Fs%WJ|(ArAFcu3Jf_R8nAz&ER)Jv9e-h6i>JAAJhx zC^!dFy`Dr`O=&vEt4v@0J%ivKlU}gL*wZE924hZz#nwTuFZ6TOr*5v2L3hao5b=&U zMx#TI66rkpA&J0s$J>Lpm-?=={!2XmigC?@0iVOhGKe(;W#;flXM`o4W_R<1l+#)@ zh_jahyJsLqRXVs03MyMuPh326fA!tvLylX+6fg2~-c2&hJqH>U_ z7IHNh>+#gmvE#Vtsl(**J39z-150pUESzgK^xXke?sK#`BW&uMpjJs% zqki@Y@lQ|2BWp8BwaN37a&eH^SHi;2+Yk4HeC^NHrdQD7Ua^6|ob?CyiIhS2xf2@A znlX`)7yaZ<(Nwo_JW~AFn5ppTk*Q2%D0s@AqdeyKDJ&$T)k>pDZ1v=AfB7K?>Y$;q z=Dx8k3FgX`bA?YtOxcE;+ezBU4|o`?GTjq1&N9Uk%FGRxMJb`jq_qu*4pE;SdG3{f zlPpdr{Bi2>EXSk%D@*(zRpw%fEW(!-T4Z$ zwzihSOklynKiryQF*;Ln|HO%@(jPqH z!D41PNN4Hmyk3SG%L`>smR~kP4H0l)c|f%{9ixcsm5U)^+~)$@=Iwq*2<=dZRPsHd zUZj{^$7?5Oh`U`c-4lGZdjf8l5WNcbCmDIums4W7Q+P~fIjzBIc>x}8I*_fqijqz6 zHp`TK$?GCvBVm)kh+E;YoS>c!+WPI6~Z*eixNSu_)EAi%43#hOw7FtY&QE# z2%ILJizE*8sx1OfY>(FOn!M*J+?^j*+T3?w=j6P8{`L%fdv$u9#A*CD4Zv#E)4a=0 zKR&$NFoO2J*rl_$C6|~v=+HQS-ur!aR?YCahU6X*5^7}Lh4)wb-D^Lw8w|Fq{b?~r zciIwu>>`&xzHF=>!Y0a*sIxrsii%ab4sQL0_RGmH-ys$zwK`HfR+i7hAC@8X)Ax}} z)B9VWz25Oj!!jLvBAUMrPqX9;Gzc7yha-Psitl~zD`Ru&jRCnR-F+1nUCf|z3>U*+ zUgEG)@mY~-1;T`P-aW(4@2TfcC|^YT;)f^nc}8sG5UOkF$i;s`1fdvJFWa<+q?1#p za5&=&yFRqzctT8UeD&SQ$p%>KzT@3N(Y@Eze+DQ)c#c>tJ8x_kvML9U42#bgvCC<* zSe8IvDEHaioV7lkG$4oq#;tWR)l0r-2jk;rEsP8;$$^wWhzz67^S@<9ZQR+~8$R!+ zV`s=9C)=KmGsPNWb5-ULf}Q2pe&!WMwewF&*&AL0d30N?uHmO88IpqX3-tk|9T2Tm zc{}~MFO%rKoI9h!&$LvuRj<*Pt4mF9mtEG1rOr9upO1YkYMab4Z$fh<>GsPiDIG_#mJDuilUIY;NsQzb!n_ z5a5j)C%Yw*6iL()y*-_fW(CiEd%JaZgNj#;npvXS;Jq+~KxB+Efc--NHHJDQFi!pOhtk77c=+LULWYog zdV2a76cm`A>@N$`J_!C2=}s#xEj6(CiT6J}@Yq8dbUdn8@*nkT!G}M4K0b7gIhh;1 zis|7vl`Df$H$t5*mM-S<;3%kn>4#_tV0H&-RL^cOw1X@ zSO6RS4UfG!!UGdTk#TDZ`R#O`mB605fs~LJ`anOSt1HINYiazB-$*$Oc*LWBlpB^1 zm2k#K!?{v2#QqLJR%EOSJ~CNte^LUGwZmgqj>^wG)Wrr?7H8U>(a@83ab$7nfavj! zWL?-{jJ&x%rlqAtFO*Pfz0O{3u~>sr;P6|~}M^LJ~+ z3o(&3vK~6G;@X6~66+q~GH)Gz_R@s>zRIMLeIA4n+$DG0p#oQoVgWYG@`}9@k`lms&x-qXm+SIYAhAh3BFd zG~{CQDpOJ1olcQiy9dLSNAL{yjm6JST+@SX%fC~S;2c~o}M7r4N2MsvB z-=pjgy4jqgCe@+UreT4}2Nng)-&V|y zV)%v>0J_{6xwjOG`e=+sm5{GMBK37q^U2Ogom;%qzp*csd2;5ND6FQv7|s*{;XGF3 z_!1qAP+eLfcbb6WFd~<8WkO>)yuR3vQ>WNh#%XWi_Vjwab{L~z)?4_4K)U*AkU%jC zYC*s7$Fo9H?3>>%J9uEE9gaES^Z5;*<#jHfx|RkF^s|w>7)VUf#5$+9o$S(B&GNk7 zdrQtopQiXKllLR23!J1~u1IRI#KRKz#52zw;#dY z?G2oxaAlia$1n)%{Y3B}U%DBIS+9ByT~QOTa_;t$_byo5*uOm90Kv>N#d&<+^MZrF zQCl9d5D4q#YEgfFY~UQqvJ|>UEu?St8FgQVnnn^mC|3<{nnq>+2zG{A1*^YuhArEN z715PDk%plVu712lOf^5P)RI<4bz!TKL9z3B8=aY9bNTo8$d=B%5MB~7yFu#Eo z0^}DhWw;h7;^+W*%~RWhXPC+-o;tG;Yn~h582ZZk)3cQ_i;mVK-WJ>ScZ7w569_n0 zJgneAUp@ACCC zKJw&6yiT@M(EDz)xHjpiaPQV#1|soSk5)_IZdY}ugN-jU_+PvENqTIIC<%=4M18A- zbG^Pe?tRKV?72DkTNP>7yKuR(g2oPhHi144j50cdWelVijJ zvM+c&-xlC;eQSd}>kxVU&HV|Bbe-87Hfi3V>y-ki*nnK+w%1 z9-~g^Fd-bvN5_3`H=bGSxKRYS>+TgoV}#~2U^AJ=r;7Eu{{vAtTHwF%jhjSR z>(k!}=Mcai$xSV{x$GcGzp0-%E&L7hr(Or;=fHCT@R|^q$Jftj@7*7Zbg>ZlMCkTh z(^pPCx-o*{ve4a;rTJ$m_G-`K*E?2Y;IX-|(e8y{4#BS^f-N&JebO{x0r}1wJJ3H# z$|tAG$n3vsV$}(7l$?jg$8=}aNUKBpP3YW6j#ywzPKBPN&ROe$$wPatEa^c!Vun=l zYr8)?b!g5=%=EVCehk!Juku|4Eww-B@6aW^tbOQG(bngM<%ic|{4})7uiXNhVwNXE z8J2V0IWhz}Q+9TFe36pC?c}S073!Py+wil@`--mRNx4Lu)_GO?twTY(0|2|Fw|S$B zyC9_Rnv)yVk1ppl*vZpGA>#yhAFck)gQ2>~YC4n7C*mp!Vn(nO3U8K+ zHERd*Z&b7IUfmd!koc695~}Vx#>YJBl3ZU%gU5PQz~s97d&U-B_i9w_s>|e_Q3c6O z_x>8}GLn&XMtQ)lDLuKCmqxmwub){zQ+H-mdp4TFZm#(NXp8V>CpD;yg#Cp@T2N$E zUFCF4h*^M5;0(uO#(P)evXdRq@=4p)(RuPXPME(_vgsZ-9RBmVwg(_<|5+vQq!?30z86(9Ia#)k-|>H;=H^$waJMWM%KtM)S1gP~BpGU!lJ;+^qg^9v{Ga^BJu}fCU65&r_f#&&ivtFn z53B!1(v9DMPQ=yt^(AJ8jzax zB5E0<)I%k{Z&Cm#9k{#yP$uWq(`axyg4N6%L{p6SHsY|u2^Cqz{I63GF&z<3c87Ig zoAI+wmhw>mUdi*Mi(%r*4=ov4qs6K}v0Opp3lIRKj;~4=1hKygHeji;l#|H*;wK$Q zqe@%n^)6>~Jj;Ip{Pb^+Kb#8_-(B;sAUHoi-^8<2Y;pySGU&z{IZTU4Z=;*d?&bqS z&Fp{B-jlk0fHrNRKAdz!Z{b~4-`2l8o)@sb`-+A4)SI1c|000M5`IWSg3KDeI*sRxJkffwv+E9|z?(tml3NVIwoq@ibLXQv68 zoXV4%oXnQUSb6Y#qfdgu@oBT=?;b>d$JZ{mfve4GZP2h3AH3k>*>zsppiuxlaJetm zef*z95+2h-F9Ki@r#(DsQThMONcK#%rf7?y6#man?vFyi;qH0fy?%Vv--`0u-xpPBeN$0f)}R4UYG^>9>d3J%uA~2DSckV z@`FEF;wLRzQnKa**yLu4zW9yfeYP6``U2~FV?5w*CN}t?WD{+3-bWosO(aNJzHdwb zWw^RWwml(eUW;}1=)(J!edi7*$FL-+SOM_eR$TKq^#w6Cbs#VQjY<@m@ecqWHaH?z zV1IuYT8_Vlm<36ax&9Y~iJmMiyx7>-c*sFG4Xez&NM4%EV#fDhbzYtBsvTR$vsWA# z!@P<(we!P{o?qMz4=Wp&Xl9YvAdm>P6t1^7oa5$5`UTTTn57B1q+7aK)3sR@FBk#9|0 z(uiZ5ntdi+&6AK1dy2PD!RrRCZeV$l89aG-hSsykFBVDEi9|jJ2!aeN5Y1ee$%T`?Li}^P+E&M=6M*`>Sn3rk7ld_$(lk=u~ZxH@?9#e8L2yf97bFsmB#tG?_ z1_Mpz808!enZogIK7IOh$<1-oW8`_hasZ@lyv3Cx2>?RGA`}>{k^qvfBx+Ib;n$Eo zQ6*3cq!D9qB4R1=7|ta&$Rz=C#QH3Fd=zFyS17R@;deAZri&TJ(X-dZL6DpZ5O8tU za$}3%ryffR`>yf)oBt&~-+G7f&lvbLCqTuLfa@Hmv5`10*e^!)3wzO(IL=18pzC)5 za8*|noa;QbEwAi?c$fdquWuz-D!FzPJLo$fnEaqc$W>?a)0tlBYnh2uUWNfgjeywu zn+xy8p;It$=r#9`ifWIGV54=lAV(xmM$ZTL?}eWd=9(OlH8-0ks4iG>Op-i)1kVe9 z?6V!6U<$oisIf|w5c#%pTg!c_Q^7s*#T-;p1+(+^KACShTUaoH1>N6q#*i6;B8X6< z13Ak~h^a?cU>x~Pb;LihUp<=WnX*nB@hSU}1?vmL9M$$E1d{Z3o~q-(B>!b;116b5 z-0yI5@mE|qtiGW{A?g_$vuL&p{ws|AG!ano&=Sg;bp4aOieNaG>&j8%gY9ZdwYEyh z7$jjMjd+hkI{?JThH3MIcVU--paXKQc@#h(5Kp`wH81P3qAB|hZ?xG@LLP7gx|lD< z&Ec5g)UtGG)timK?*x4p1;GG=Gd{*Vz*kPp0GRz31ARQZvS}=f159vcZ*+*OrgP!{ zPHHEP=g2cl+;~NXg!Of|(PyvKcQPQdmKBwUQ5*xKn4rAzG>n2Ct=8F(lB6{Nxzzi) z?=&}N0ZXCx1$#Z_@GLkN*eOmbI!lWmY&#U&v0XVvjk#emwd3rvB3Q#1g_9fjEIc@J z3G>DR^?lARQWk(no6`tiGgJyKWUO2>c^ih1nt zQuvoi4^xyFrbl#}J%|N-W53sKja0w8`8^Jl(b-zbZfflJc)bl+B352~?iB@lN^*BG zx^USJ$3y4lpb!TOf|d1c6>Zlwvk7}@qEUZ&YPeUMw+SgoWVOw{GR?EgvP)IC1i8%!GHQ-qMlH71^ky0|}X0No%t|e_r4HtB>1J z=xOp?I5q+Hs6?{yXrvW^;8-ibiX?>C?Of@MXFDOugIi36IG+4=lS5m&vdV0mTPZph zv!)i^8*1ZT6+0qhJI)Ypvi8f#%`zVOxpc5c&SJ&g=8u8Gy3)J%nGlU zZBCnPa*r;GXpd;Ah!`A?LU^{<_~X)_)8}=VDB36^HA!K6AAj7Bav41++y$jj0}9=Y z?UuXI>?&q#C~|VBWi5&xA=hMuq;hjXJ1mT?;gX z8Bfe!XLs054bGsbKwe)6cM!9iflq(gx(erVa9fkC?msDn*`ju1k){dx{Zt58vJH#@ zQU`<+$dN)^p++D6NCg@f2NsKr{~{+r&ER82DKVaPJeF009=9?=2w;KVULZtS4?~dG z19JpWc8khF>wJFI?SECj#-0V&>2k(1u(N+99nY#CGgo{N;su1zf?=b>KNF-x9>_sW zUhoI{1yt9X1u)H`PM52`x?>@p&q1rhuVjoin3`7zOl|UKf|$r)POcD`;|z@k?DcJ> z$nkJEhiWD}cWeCbInfhi$c-cNksu0r9{QL)fyargP40ymm#=?FNkZjx>kB*tSxasY)A7B0Z4I5htuA%67g&-6wD z0UHU#53<=P5k(xqJ6it_+#b*+H3Q}t0A@~2*MM!>^&lUr?&DKsI z@^|Y2!Px?pt2C)?iBvO^0^l#zCI4O;11O5h9+_V()g1efjt#fJ`uOo9KM2v+@>OR% zQ2^Z2mm|5E(hoA5Ss@g$Ln2#BMj5Cs0LTj;U=t8Hv5a-5O>ct=YR;^n;Rm^W4O*6u zbBve{|9g364`I$ib@|$HG|g z-mS|Y{Hm1$U>XoDMm+c1F!Ay4p>Qt03y>v(X7yE7ATjSuvKGn8%Uw>;{NKAy^_Xi` zdXp$q3Lk8k*+E zh=_aIp(giytDca9Qpo$*z6lYdJepKQ@_wgZ2n) zmeSq5ae$=6Rb(#&9rDFms?Bfcq4c)L=mz;GNPE2$dH$I61(mN%FrOcdL2DD{VAXJejRdO8Mu@Od0;ULYV#=G$TqJLR_glUL5ke}~68c)@1bSDI2@n?) zsG`cB8u%&rYAXc$UUzaJihw7GiiX}ONYF>cc4D8h>^N@C#2%*nY2 zInBID-&`G~IG`>GlF{gxYV_3?S;_p=L{K_BJbbFQCNWNL$UX2mtE5Zk+`Dq>_9JqW z=c#H~`9jxobva zDC7mrrvbICQmuIY-e_2fBbq-MXo5^u+txkt^>K1*K_seW3olNXqb@E@?;NPLnQeZF z4F$0oR4k|!^K_Ktivu4!G?YX@b$2c=4TtU$lM{&3qB;|ktMty1WO=T8;W`jZaB201 z+Mb;b{_;ExPR*gEr)`~phTYojnFs4e){yfR_=bJJox}7 zotUD@8Fa95uo^xY@KEt!YUi#urmg)!ZX}_lXY7MzFRJt4C`L*=!=Itd3vL7{x5bUW zE~L}YlQM!Zfo6)+`?0+0IC|{bs{L?rQQO450v}8uawYIr&fIE*d+!_KbJYR*9>wx8 zSR81*jU*L>UZT)c^m`j+(6%B|;5w-x;?JLWWL*+s@ycNg)iF52*(z5e=nJQ@*FYb~8qJ0Pa?Ip*gyA=b zUijIKwacV1yGdE(Po5X;G7WKg2j`aN>RKz2KK}30o+U2})j` zu!%s@Mm!xK@F5G+GA`1}>jQd6Wn2H4Y0KSpt_C0H;@Nz!Z->&D&Qd*w*pbG*2&4BR z1~GQnJfV#PL$(0zEZy>-CurDvZx{27@;hCFzf{ z`5G})J`S)~6`v7wW@_gNZIF_Zsn^L=yyq~?`}<9^Bfph%*(bogo2&#W3d#i?1ml}k zRxOegjxv`W3g^v$sU7}ca-#_HmGaA7=N+h{Bt=rR5|wwHy1NQ_dhhK$gVBqToy#AG zH?ND}h1h9^6ODIa_{i|GLv@lcxf!ab5V&1*-mAq$5?v`Ame%@1Znv#mtDZKX?s@kE z$$R}2N#3lgGCiM<-?J^>EP=Gq(h1l5IjIRTZS=JV3fSKZW3bX35)EN0C@Q)EH=H{Q z$kjGs!%)!?DCyNY71I><@`35yds&+}8VMx8s$7x#Xk?5r#1>MtIB?SDDSrKImB9XM zIog7fba_nWV*?*#C-44knNCr%fXi&@awH*xn{g-NCPE1BQa0@5U}zS1LZJImw*4y{3v#Ach%?wrjfBt`2= z4{~Ncoek6>v@DbxRy(Ld6iKS$E0E(%XJ`;(&*R{yBQAnYJ5g>4Gv5M zl~0F1=jo$D?W~(j8l0x{K!}N#=(v0NlR{YWnl>txCXRi^ebCsqF%O_ztl))sH6S?6S zX)ec>4NfAC?{DkW&|gdoUH0tWS8T}T?Y?hKH74RBC4^yp)L-E|F>Z1%E|5?W+J|kS z?O!g>(x``c^s@+sll)$m*;2)!5pS954o1N*k7qyI*u;6ySe;fTeO8e%j(*%`aM-pC{86GAtzM@YJJ~pgP?8t^zu``z%RoT=-``9}h zfsZ0H&TgDtCQD>fbSem1Eg539<(R7)tg?g%!v@`|o%igRacE=+P=SAh%8^k~qvg>n zCgiQZ`XVgXYOF)4cx_N||9da*Ca6ieH$RV4@;XBCW%jqQB&r00?S;KJqTgF4vM z0!6U-wtye%uY_9LIQ@`eY;+uQk8=7k3IfNYjR9jm-#Z>ZASH3*^W7YZOWB>P>|-us z1mSSOsp&D#tsW$@Wxf&u94SdyngqnRDLShfmj=i_GNM=4Iv_E;k*f$Gk6(d=fT46c zp%MmI^ikwIR^306Ycm90lL3w?*KjPWT3Hz|UC7pPAj7>A%hvJbbnIcGpF@!n4HR`a*;mb7+||6467+;vBy98;8oE1jF@<4EM+5!A{OK&zg6>Zl7Dt< z_-B24RXj{&05G;Z-g84hWmY0E3WlD@4}87`Rw=D%tCG7>_LS*j^xglu597wDV(~%U zov%J`gMpZcqNzF^0&$Lk6-Ujlt+xR<>Es@_ljpdM<#*1O+JCVSoiDE?-#BQ zFB`J(7vceTMzUQIfZQiTt;da}fm{z9y&j5>q(~xo z`uRXSg&(rmrj{XOh%2aJHvK_c-QM!A7YwTOh{q4HHDs|n*7&@?1SZOC2jFALAlbD{ zke?s^djd;*AVW_(H4!b~u)s|_MpcbXAdLPNp4;HFcFY2hDpnIF^GeAQbIk-!z<5Gi zWWM9fhmJ+<(uZIx>+;GAVOaE>`cPKM!+?H(DdNQtkXM=yQr539OLCQe7wb zGLwptijTziF09|uYH<4l@|Czo>I;uS^DT#m_ixThyJViU0I;(MN;bnU=ZO{Y?C@It zjYNygFR10I=w#(WX9J0OIN8UGQ)r9(qJ8wqUdFe?zA#rrXYN8SAV*Xu8&-$j5XEqM z?+B}f+U*Df2VqDzjaZji7Vz(Hv%h&l!Yq`Tmi{XyM^%UOwyxPwIv_aalCBVnAJUuM z6NW}X7R3V|V3fFb&5H9>rM9}aL7mukWh8 zowgYeQ%y_pLv4FGy8`Ym$-;;{alIG%xb?nHtRnZ>5-uElnmfc{c~?7+w?4uX6Qs#! zaS{F_o;rZ;hz4!gNe>wKuWJV0-MZK$q*@5! zO#1!jCi&r_teOIc{Tqcn#O8Y9`Wj)2fiz zocBYidNhTflDhQc7itI^J%`5gH7h~xQ5#U^H(AN1@Ram_0JO=bmr<{IeRhQ`R$s*x z>je>DEy}G=^*{qf5z;@pE;Et>P=&vuLe47=95la0!t|N_04zgPmv1bvk3t+^zuW;8 zBx2VSP6-v)i3w&V^~&8?Pm+oak`k^U5Tbs>tB~A%k*JSl$j%FCCPv2>3^nCL@tV*V z2!ppg_!_rcxP=^a=SJ{|dav?`*Gc|KH$cinv65Vm%*El0%p&$cVPhQ^ zp4Z6I?}!%EVTxMSf8*m`@5xM?<6#E^(=G0ns^-i|GUHWRsL1JJDRg~18Z@y{oxshp zpRVx*rr&FdOR5=k`o)Dghpzz8D8E-iYxQYZWlgYw_rHW2?I?=3~Z+}Cj{Ar7H1La;-fmVWdbd8yV&`p?$ zE7h>kzwDYHgJZ`6Lw-eK&WDd?cQj=mOjXYIhro{^K-~?;sW|4>o-PC55B*t$Owix0 z0V2JXFpO$ETQ=H$Hb15#9bPk*9YAUBZ%&ZgakT+pq93ZouSw?ZjytdA%sz3j? zE!s64UhGRtCX3Tr4*P)kfv-CDbU{Kq@{Rt{ppdDOVMEWqFrRV-SnbY7CC zU6U??NBHyeU#D9?REe*NRAXn#T5P#GqFeN&)P>2@+p7aBZx7R82}kSb_>M55{HR|I zupJ;~g623yze!83JWV!FcZj;4O7@(r+=lqN-ca)B%ts(mkN-M1yd1wVyUiSU|1rePG*eGT4Vtfr1Z zwtCA)2UTYymg@fU{6^mRIpy>Nn7dmPx{W(ps97XpkMygMkIXgv5E>yd zLmf)n1KjD>;1_&X@xHRC(zzh`S$wmpl%#EORVt1Qm~Wx@uPM^zI9tzq?Q$gnZO8<~ z|CiPdSLzou#WZ|kpvZn3M{Z5HtoN4m0`LWp*8>fgT9B$uH$&Nhx*zn=?L^?t7iBj4 zV+hHag5t{#Y)%y+R7`xgKIskn$4>;th1J^IGv*&qY7px1$sfC@U>G6v|99LO`b7!4qNdeX=G^i2^JHO6Loxvm;uCh1CDW_rijsSv0 zB-s*x&%0g0mifKHO@6TQiPHy6vhcECQS0Ngsxf8Fccc#_o}dM}*(Yg}gMjRBUw)nC z2>=9^{fpO09i#`t&zugR@31|om01S-wKw4#?wxnx5;tj|eJcS`%M#-e5WF?$4`Re! zR|v_Xjs~7JJJGN&3S1eEp4b7YU4NEnDH~)q4Tm$eH#+D9&yGRN9*uv&j@k<$3 z$wN;Pd-@VR48B^R9y6~qS~S5sz#l@_)|8Wj$KMi{Ml%zWxmo{`q!s9D0~|8x1?$|b z`lBBT9^AFg7hd*v%fOk@VpAkkcy1xA1C*MlpKhOMw1A@1ShH0ne1JKtqe*4*l!#S- z{Nu*usn*Oshg_zp7U4@Kxt05U$W{tx*IiL;0Gqis|GShVXA&vz!L`Nb^Ja(gC!8;E zw$;D-!G#kNjz@%|&98GBA1i^(Ywsgngr1OGV5-Mj4rS^$N>Z70P9g+9rJFBVvQnpu zZj5HY`fH5|Y9z#!Vlh~XNQ>gv0w$C-OWFwK*j4a#B*{kG({IAoe^(Yn#lB$ErI5wT zio?dX|mhJjE zCz)@1jC~vT>v0C*@|==pQxai+xlP}vL2kdNa&P5+?9fc{P*g6Xu&8ut9U2gm70EBR zXh;Grzt~=#GFai7p!7X^v3Y4MO+x(p&H}A>4s87$!=nig?A!^4R#>Zl_y7Sj${khz zqjzMS8Qy>O*0su1J}Iw9#q8vSOaFOhuLmk<(x(ZX{$r0y1AOJyx$*+5t78QaKa&7V zKc#L>ghq3UJX?k^0e3R_0zxEQWzD8sL0{jSOFkm1h!s+4yhK57Jm!0QtHVSbQtmaK z6{b+dG>Ni*8u^G|x96l;XGHdyQL{aZaExYJB~!|NcWP)iUmnlud+(7iLo}WQrcv%7 zlRcl=i1~_Ws42CBgd2g1a-CH9mzKn^O**Z0Uuv}V)G0x+$N7k{MR8;DfM3^7> zQ-vQ0ill*G*hkG!|SA%y7Kc=Z`x$vej5J61aLgw zX&KHUpyG<(RWSis8XDS z7c3ZJ9qx1!fgNe^jzeqGq(6Zz_Bn6_-J?4YXINs*ky8OgkKxRmg&Dq5v29jLDMB=g zT`F>9I1Ig*%sALh$6BXFzdUnxQu|TbTF%0i3oixg+X>&V()VL`6x)bu$NCYg090^V z=gY79*SLyEAH!f6BDd-t>lhvMlYDMJCR(HZ1S4sAm~hFpSr^$nmICCHbB=Mp*j@&? zW@pQ*6tV+zQ|0lO>knf~uEQi6H6#d0`-1*f^UW@Gg>(7!L9EmWLz;mH(MwjjQ~L|H z(N_#bHasJTb*(=S>uwVW8G>B*>TXLG!54l2nI&g`*s=2Do&c*p7rj2ndBa907}8?(ItqMs%SuQ6j<#lT2{5AH2h7> zQrNMg>!61bB(^RQBf;z*tM4i-wuNmUqhs-9xbv&^!Wa#@>6cX+r)?>kVP(D7;XjpO zb*%zH1s3ClSmI)mDqHH8s`^vN;9V_Ldt{v zO}JCkyV?(pi@WtAKliNFJfv^Z35?Zb^O*fV#Ma2gQdd+J*|e#~;sbnGr@=>c<>tMv z*-Vu}lCdvlKd!UeSl&FoJ*|wr4K*T*b|{EUdDZNLo?zrvIIp9!7U`~w&l$~4vv7)N z{Zc0RmvEa@JRxpJ$>>x+beZAB1Wxq+vU}0XRSj#FbGHQ+w$_T519XxWVCnD}M7uX# z>XTq1E1b+NDB#38_WO3d>QCq>erebvCjL`*pubGLF=LXUKGa$^#opsaEN6kAAf>yO8T3%UAu-7l| z`Ucr<^h_q8qRMoL1k_|3tcfF4dY&%Epwhf?Hcwp;yEWAL)Gb+4_Z84X` zg9Yo09zI_!-+mS|5WYK3rWOA*ktwdzR+Cns#b>XLZryZ6bT}?)fk0WtBsVd}Z@Kck zXx{T^*&ByT0Z~ATDnXSho!PY)Bb`0_wGVH44yWRzT_!okQM?8~Kn*k2t6k;dd^w`?8=z&p{BTY_a&1<`4=Vwu$ zHiL7tPh;Q4Y%B@-?tT2CNR>9BJjp*-KGLW5`?s(MC2q!tJ7nK}Gk3Mm{kM9`okfB^ z!o+9$22~u_<~_ywz1Y=4ypSWEWd4;{SB2s_tP(#m{PB$`A^ISm<-1sxRtrj*uGE72 zS8Kz1L_DL?!lJ(ucD9%b{mss4{>aORI56KH?D@_;NFUQOqm#Ht$(J(5L#(uI>?$dA zrl2BdjVN|z%Jb>wL!bUgeXaMlK2J1T-U=d(N@Yk`rYs!X&raet__PR+ro|4qtxfDm zbJ-Gvk3r4sZ;(zImA#NkeDlsk#o@IO&z1kB4OAJ-i)kOm9DqoL2^}#9Ug0Jk-=ro0BL{E?( z3mqKP+nxYoY%Ff1NZxT2Mf!-G{-+^b-DxVQ>Xh86wQI7z&bKrJ^n3eu;=vduZjN#lP3J`NF}o$2iz2 z6E~#M9Sc1fC5!+o;p_xoLJQzkq=Z;JyekAr zX=p3udU6$Lbgv0oiQ~|X0T_jn>xj#DjK4ZqHxjJU3C7+Wjuv(20zf`gE5v6*0ONXT-W*+x7NS>zt7(x5iv zt1@}ECd)}+@h&c%cg)l&hbSrTjjb+OWyKd-$=;M`bb zt>jvN#!B9<6R5WyxC>?Vv6qH$-nR8?U)aFf$sE&zK{0G>F3~6g`kvi2uKnT4pNQ`t zh>2QJ_k)n|X15Xzm$1dUWT3RP$ZWUG6F-k@N`y_1DDy)AZ07DvcXs_sj32Qc92aUD zUcQjbqMn1vq>^rEc23s%6mf1nv-$LKqtm+RrC%CC;FEPDDnNI)$i- zzLHzbZavmHwYzmrXjcY^t&2%7J#QtF$ooT!Ej#^haLWArjF&gxghs6QIKoc2`yo4vKQm{kSG{X{b~D+8a#7fS&n1L zLs5WCY^V&&CK zFgCatM&K0z%Br~`zi=2a>0=ESlLv;D!bWy|1y-LUZXzJ5RAl`V2V za~I<_zLe`#mej?63vH=jP-zH0E2!>kd$MG;gVs>c^^x`*er#5260o_f4RqOTmn8Hb zSd9P3^#=M4x#L5;xh;YfiOh)x(Og3=bf+w)DiM*GG$1HQie8%Pb zJ8ChmJQiUMx$?5IYl<0ZZ1lp1p)*kJcA40_oOF6wvb0Eo*8t@u^Xl z6F`OePpEjaQ7o^J4LEd%QzYeQHMVkz-M4wq6>-6RZDfEqpr9l6hf~13$51C`5sdb(C`rjQ)3AzB_cx#PYe-v+ir#ctOV^iX`ewH*q^W~u5#nUa`?h0_Q| z+J=gxXw3%932F=#p37tubwx^hk&gbfzNkMvIlj>I+nt8YZ{Csv%$T2Po%?AiMJ z>?pq(l@Dl#uh{f~hs0KW65~be^lBUn?O~#w((XHq?j#rafnFxsCiW=j!DKEIh$R~z zS~Q(~4=>Z_)-W!*OF5|OJ)x4zy1c8 zfht!*Fx}A_Q2L@)QO=^!#D3r-E6A8y%sfw@DzVCo6B!?uS=-St2)2B?Sz1Hidg``< z?znW~`_gStljA0C?^{qdwbr*U>~a%Tv3SG&YGPlLK*KOsrP4l3cG|w)-ayhOn`u56 ztgLmutHSS*D#{4&Gsu1_B!P%Dm-N$B*ALP5bLP;n*B>{sn6pZnlIA~uI%!Pg0*c4S zpn7Db6f`s0=yo2z2t3`GjuI5ANNkqbU8@L@BwDAyAGgOR71=b)J=#GG<{T`r4v)XL z2Q@U5FJ8Fya_cuG&MfjfRxc?mZR=xFa-W05L5l}i+#$o zgh!d#Wu+9aA5IXw6|dwS`x>I;s}+7Jt!rs3`bJ{n=V$hS!(U~HjWEUVyz%)!Q5fN{Tm$704i3@dS zt6%=k++kQxwVHV~97Gnf@qW%GQ#lQhewDC+S-ANer!T{Q`jU1Dqn4Ow62%f%mVb|1gn{lrWWlwk~Bx z?V9Xc6*{Nr;Q68ZnwS@$q1(jAm6$bI&XrX^W*9TlC}=zFG&l4x$-Nx^3!^xfr}C#! zv=RA_xJnX{H4MIP+-tfcO_?tKs@CVmO%96BDz@V$V^JX9-)yM_m(t_q(Yca zMrcE!L#ZtGMM1P8)38tDORxxQ;ormof;yhPg4Plngv8di-@-GYyqg_59TT610Zy-l zUnzc{iY2PK$s}p!+^{bgpBa&HugeE8W0}^T>!L_#q=##jZp`m@5nXI2uqg7JSDXh? zU*-SOR(wr4u>IOI2~vu_;{wz`zEkx--@B`rcRDe*vDtO@CZ%YI=(QUNIIs~AzEb3Q zlxSFa)vbg!*&mtI^TZ-I;-mGUs^oV8dP*MF@~JtKveWKs7ie(Fii+^Z>^nJkl6R6w zE)L4KJ3q0zj;X|h2*US6>+V2>OQI^Jz7DAcUP2vz!cQX|v@g6``^3DQ)W=Y)xW@lX zl$v-7qKuU33idLyev6R$UVY>Sm_fUXKX5^ZG7Lp8?jz2=KR#%~@r?VgpKPprqStzs zf5qtwEk6$bHL~^sAkP*FlRG#@IAT7=eSe@2;W?CMXDEV1AF9~b&e9w>rA(|C}`))t9i%Il>cg zY9OAq{M*mx@%{z^c47J~e*Q|Dyro}t8QuF&jqe$9IJ@A_!Nq2kEJoVEbF*x{Zk#3h zkCj_G86xM8cLi3<1}5aZoqoopdA&k?K*Nb5Z z4buO9cSzShaVA71LWqxlT2^;%M|qGDW2vG_3HZfNj8J-c{-Gywe6?g$qvsH5ZhnI{5Y0{_?=+1%ZMQE+a4Qn$`!im^N%c7Uca3b zGY~bLKbIw(G7tAHHFmDZebDYw(-CCN&rbF=>hEs?n87@ufhKjfN)MgjP=#w0O#OAe zg=)WZ?z_8t@B2?&FT<`dusTyZ#+8$_cacV^>mfGGiNw0z;)R2D;l{)LYE#dflr-Dg z2;VRt%o*Z#7QT0i=fqSt_?k1{J_P7HzmT|&nHr6^(fmhq7WPyMCcE9@S?-EKJL4(^ zf#Z3K^X`_)OIgCW-4?0D_=+)wh$t=ntJdRVO-4>8oe_3wK@%0ZP~?{S%C707 z13holzJPk!H3gsS3@x1f&Dqza3-_3YTOe>W?Uaw$@zyhv>nRY}A5xa_jm%Y|uT%E} z0+0WmW{`0?Yiz%-^+APsk97*&hJ;o{p!Ia%#>-b9DuH2$s*rN z>LB<_0by>oLJ14KWuX@j3<=aJ{rrR6ry;H~b9jcKnWGiqb-Mz-E$D4TppLJqf26V> za(pjiT}SOqdEb`{`f4$ovU&leX)K1vws(PGk}PQ1SUw5iwZw)0>vb4ei_Kk3?nl9P zr}!;jjwU^{kuyp02!iWm+mK z=^QuTVn4E_S4_NO0|50<5J>*mao+iF=Xe;+@|<<=h7jt6x9t|dzL%46F|6P^a%Ffe zs_`ffh`$FxRhqc-ra$@H%7I=@(BGmgRfl)DH;iB(bWc9l6;2sR9aw>&-jEZ+K`?ca z4SL+>m)ds`)XDO>Lz%vdBxJnki1k~gdeV`|CC_NzM#am-^BOP&&Ta5*eG^Q7T3yKu zmea0w^+&!aw`{i0u+?iWlAHhb*KWSr@=8CeSkH<$f}11sYqLfDOSaHh^7`hQ6wKH= zVmm}T`KBYU>H5~Jk4si|+lbzVaouR%Y9yPfudUVPEtzL;{a}f>!T3R{j2v z_f}jQeU(;MX~8wcM*d1LkRj(jeTF%qk9GaID?5)c3aDq=la6Sbsc5PjwYM~0r@313 zbIuc!>Xk*INUoIzAM;YhAMD0`)uvO;`Q?Y-SY5OISrDDR@M=QjtAXZgaxSt~b?zfK zg)VK2t+n^1ECEhcC&%Jn9sI8Gr++ALinp5XZ`vKdi9Dg1Y0C1gBnDDmE_()T6A$`x zHx7Y*2bS^T;UMz$#MU|;Zy%rBuE{5eZ{TEca&y0XvPe2bqd6Jsh@9(t@rDU1r-?{Q zF!cGa3eofw&hIvp=34qITbM$kpMVtKR*~>=CZ+FfwD{GKFcADo^lH zm3za4^W6Qa#MRP458~fMvRY`2B?Uhcjps13nD4QBR8~>Rr;PL_)r;y+&phY8NUZ8O z1`Yb`{F;uaY<2p2B;F+>X^hGnSpQ^-G=ce9wRRIjT*tLy)ayY-Iq8?zPhqye;E#1B z(S3<73Jg$9uSn3Wyt?s8ds5PbXmRIxxnUXuWaVjCd>Y+NA z_Hv#N-eGa;Y`5dX#7JP@E`Kp3B@v|+ZSq#qQmnMc+3pB>@v_nBb5f91LMuE&%?n7jES=U0fx!#==B0jg}@#RQ!J@Yn#gRqEmSlZ zCt0uFCn6zvzih*Q`7P{e!T(hNpU)QGmB;>fti_X5PApGn0mpQ2pCsEG!KKBc?`$os zh!^1tWpN-C=xwkqL)sF!QL3qT4) zhyQq8i8r@<9WAvCsjcq5eVoMa)oC`00Br%~$rlrl>sau%?)PX| z!Nl@5JRSz@UDy+-Z-z3OX<49}JK-)n`j23SHIZ{s5S{1Q$!ji!R~M&cU8onf=3jC+ zS;G(%^Alo@ky4=!lpvE_V}~W9#DU9QvZ%JI_}7+8QgPJfZd|kOvB#y4?mDNrtEz#K zk;@t9;eplXEe7Cyi5t`KuY-1ujYcHCl{xr<57@!yDV@(i(wp0ZMceoFqak%sE(*EkLcva%5x;y$cn=C4sLf z+n913wq35``LXmEj_<-x@ndqsgRuyGiSr>D39&}jx;@WY`fdt+&FHH~V`N2-*424{ z%%oVo{rWcN{i@apG?G+eRVZM;HcIg0eC6;0FC%*NAn%y)(Fi ztN;2@D*kLW*;6p%N;kr!TS6HKhPKa$gz$|Ej=hkL_fEoR+F;d{OYq#D=xO`=~azPoC~v0icVcP}a>U$QVw7 zN!{T`9@fQ)2=$S@JNaDDx-OS53djzak(d`#=25HB3kX2}jvCn3q zwams9Hno7gipFTcXsI5>^ak;(2uU9i{a)&A^A!SP!2<-?l0@LYi$<|4rCXk;a? zR}wtP^QO5Fewg?nzckcPNFdj)Z|C*9bPy?>DSN8`5W5;E;Q^&nIrr1h8BvPh?5?h^ zna%(|V3tb5!^5c?o10s-vjBM9$bt@_kTpQ`?$vVn&D0A}4^ibr#l(C0@QRA++bz`Q z^Si8dr|y^)gkhw$9CxeF>~tU7%~i6ZAR`YYqxJc9<4&6YkU{ug*1v#ETuA45oT*)em8!uoP^ zdb&9{0yf5klym9vs>ZK5duQK(LqbC8P4?*SjGq0kqGT7Y;YdctKl6&yA%rFOkn)&^ z_YWqA>@GD{IPFJ&cUaT{CiLCjDMJmoH4t%mflBsU_}+OAgvF#Y+P#OMAQgl2B<=2lzqDO*1~u6B0( zfJjSA>%;Z$!}uDf?O0F|>&R7G(b&-A6nG6s3S&Yc5?Mq;w3W zQ86jLDiXoj*Nf?GmKcx^iR-ZLTT`XmsYa~eou4jI#4k%JwD?@nRrim#zXrTYht&DU z<%Q4ZO;D}SNGu05nV@t7!)s2%PDTyt*?vX@)9ADN8T3s9eH9?NH$|uEEkb`RbaHUB zetUdw&QJbCcqvI%lKVgB#U;810`GYdpG0CjMLp`!idjj{cN;_d?GUuCO;2>+hr6p! zK?0JIBzvBql}AD+j$taAz8!23mL6#9GH}Wy^P# zoHjjQUtf%cnB3)Ikq5OXiuV^ZEa}K_4G9N9!rK1)u@F&g2_4G@&0D?(%vHZMijZt{ zHHG14ppw4qgZV0QD(WIi6iD?S5S%fX?+=@FAkES~w&EVy7nYF~uw<2wJFp1;#v&6ZsoSN=E2phDe{7JY0 zc)93YZgcg|0LbEwkFna*Z%^L zI^p`b7yG?==}ci<4RrA#$PX1CRvL?3qUnVI40e6I@IgU01ltrY6wUCqddGoYlIQy^E__?~yny!0d89s{6MwtEaFb;6G3m*A zBsdjJYYg4k~Ap{nzk6xl}}h#ee13o%g0ysZbe+Y|`)!m3exSNML?g zTG3M6toIFZJfqd0s@MRVo+@l0jD1X0|SDu>J7% zF<`J@#qGOPgb2LjhVl9pU&Bi>WacB+?iGo)CO~50ZElqYC00{!lU9uPNks)cn%kbi zhyKx8H(l<#+A{xSr6!e#N|XcjBenwKOwUXfX!NT_6kr{SQNfJ&{hIh+m5+EXiUw2g zx)05~`toKspMzIFI6e^K!(R3pi9{Y! z)_b~e&mfxpRIDQCmCNUF-|B|TUSuSSIs8G}?p;^97@bzqv;B9uY!#@uWbO`zz&vxF zLlA^2u`=g>(cxeRR3CX(HFIl*P5|$Anq~SY-jbkY$1$hWYz&fTo}V}p8PILtR0R!< z9I|IapI!ii@@{_1BDHiYVzD)>?`y`>1Gfa@Aj*k&zs7srwIC5Dfk(TX`i=rN#|3mJ|je^kY~nrC=Yf539KQIl*q< z5(B6xJ+#Mp#%O0@ED8&0ANFlRNaJfx+}@Zr6YQ`e>@w@b-(u~ zx}P0VbrR_Nz!&4+#Vaxzg%_?d7o1nvae#?Pl%A{+RkG|M1vhU1EQbqSg`;uB^W0w7 zs$ z`&51xFWb@jB4BI$HYCT7L^6~arKKwKEPa$`QJ9avPFSTjjY!1fUBJdpb<365^|>g*OYbf zQ+QOA$~nz{MB9=NHoE55)z2xQX~um1JobH$6P))NFsz5x;2N*DCxHO^&mjfe5I=zW zB4ibjV`LSw>9Ar9u*&STVoZgEr+qU)h;8#lSlHa}A=&E6}$MwD!`>EZ?26g5U{l}_p z|rbLyT_tH3cl{k=NdtDJwnnk76AdNtsg>{Iv*#eMtEXLeG6c zvRa6@YWXvjsTeU$P?8Qws==YIuI{6rF9VeIdtm-uFV($52*GXAnSYeaj>Rfmt{w-v zcXOo;qj@iCUkjHK^Kj2Z^_yZ|-LlW9k!?SrV0NosDBqvc*Se5FjO^pqC|46|>rA|fJ8B#wr`AkTe|?6POixfcMNzYMTQ|K6R$mJP}c znyP1j7Y?L$MNCA2m*#;moQBxE}XyvxpQ4@L;9y;Lo~oA+LE%ev-fQH$SrD>k$z z2(Vi@^$sLR+2K5kr&PM1IE@&cotpllJ=xdPjwpFK>DK-+F*+v6_#UHt=q)WRT7W4~)ZN{!zjusqpHD$G~ z(Gb2?ILEopXNQz^+jJsoaf=Rtp+G2zO-$U01Ua;_I*!nya*_OcsP0(+<{<~mdo(Z# zc>dno-@jc9#`9gfdAc`;sSg>TqHCS01BmgJmfIj)D3EPExnL@Ux1ViUpe>zGx_`6H z`lPcrK!D+rE#vTa6R z{~NbOlNnbN{AQT}KdfHVU<3qwBqdp?cR!->ixbU!NGvh8dW>{*(*2=C>DCyK@7HRz z(}2nYXWPrQo}Thy=JDU0V`Cz3i%qTBp)r`!g+N}m(5=H%HQobwkA<>PGX57J=%*Z8 zpy?bHPsNLCXR5hAt~HjNmEEn*K}HuBT)YgpVkYnXEHGer=ymVn9MIOsJJy_Zz4tKE z8g6BlkYH;PDJdx>4yOQ%=LaGiAtKY%u)oNviQh)*GrD!lg=RTNdUw`jP6ihntW7>8 z@I`^d5)Y~C4-L~jxt?N5rT^_s8%`_;;-T-5%1C~`ma(yMG1pKqXy$T_1(AsJ8@g;T z=aC%6U}K|Pd6}or5;oCLFC9V1ys4fNw$^ofDZ7^aEkY=|O?7G`15_ItD6AnW+&?Y6Q5 zBDjGBXA{4`fK(q#Xdl5BnQAOz9Y;lZqu1pH2^v0Qtiw;J?I+6=0ZD{Ot7W!&4$Psq z4c<%3b>2aqEf~2(a%<@3<(XZPz((?{sD(_`pp?{MoMpp6-+Qy?g8uX4I{xRoXwB0Axig_eO% zXU6b}LO>Q22$9=^Dft?F?v}H_`37xpA`*0yPW_PX_w#6N92&%i(mdDboE8Zl$xJp1FJx4tkiWN}s6~&36ndgpBAApA0W)gd`{qt(DbA@;q(C zap|Aa2(U+48mK1ExS@+h`ijmRe{9MRb=ev4T-E;bAECavg)Sm`TOOs$9RbM*DftMH z%GU^i_9T5wzqM@Sbmu^vv=m>A8IdWBwcwnHc4u`bGuF=PM|Ga=i7i-Kl+w?c}5&csfpC@hnSB9t6 znx~&Zm(fplC2;;HV4%G+br$!W?{Az3E@bOj4z>Uf;6N~}Rf}~OJ_)PB!$QLm>xr2X zBq43}2uVGFmDOA!wl*0A_hVu+LHZwivzfY8-n_ANfqM*W7~cPLYfqL_!bXukOxjrd z`({@n9Q2=9{r0$vHFX~87kCT*ZtX!cz#wiBfo7tW0`|-!B@C{&5C4LwKn_DF5`-?K zB1E1L0=(>IRs@RiKR*NHX5)nOFR}&yFZ@Srq$6TE7Xk44}A`!`9oibo0 z0D<=pVe5&VaK{He{14zYSbk1`waXd-?B!06QES}TE)XZ<1DaR1cDb_2mExhi24=sP z3D4>LOKl{Ev^{%P6Xm)F8nimTJre%!pW!`kX*Byy3NvRQt%?XT@-lw`!~{fu|JjCa zrC|>bCP;-MC)wbn3qKLKbN7F~Tb>;cdX3iUUYQ;A2}O8^(kmRKZ@E?t_a))!6rx{y zK&X;>yBm@i+XEINd`Xu)o_+(_9l$b%293;fEq z@9BH2dw}MbGbKu|+u*h(p}Q}e3tsflW!Hc1T!ja~VZNhe7yZt$ff%ywdIIuQ`pNZP zDNbmsb{Y-pro3q|H)X=czZSvpG0W#PKMl(F6TAhwJjB0~2Xa{;iAmo*H|5dwbx-g2@l6 z2DQ3^RTR}+yqyO}9C4%F@}QllLgDIc@C33f6Hsb0TR1DTU}?dK8l{F_^7h?0DsCLk z+jMhH>-p(!QII&0+SK0JGJ+299D9HM6l9BQy41bA?W^f}_S-3Uusfe z`QU-EnFMCr)Kbj@mT07>7v!_w3s+e91N~@jpHkC)eCCOJO(ObMGo&4x3gp+Git+@C z%Hcfk$UvakS@22(D1Q%LyYP9f0t>eJ@x>D*KelHJksh}mr*$tTa?_o;UKggnuCa49}v5CX*L%t~%N za6rnln7y@x^f%Ynl}amtw~7Og1r;6{FWN-y@6=bL-5ZocM9YIIyzwm_` z93AcRSMtf)nP{FuAE$*}g zx3o<>PQ0Z=S#6vM{uC8!+Av|dPSG z);etxo_+H7@2%%XY-m5_u@`Ki_HlQcT)>W!Was)rtvsdEOagLpCc`0PXjW6xRT&Vv z>?Gp%QhPTHbI;ww{eP`p`#;lt8_zt>CU=G?5^ffAO3}gDEhQO|L~5JnP&w1gY4_c2 zPY;TocpM8Ih!6{#944}8n5fy9V`wp_Q9EYtum9osygt8vUe`~b>v~<+`+dFN-)U(Y zze1N!aCO)f5%r+Nm;#844%(7FJ*4T+oRi#ZmHL=7_BN>DUyC&JB0qn&_vgw$)S~oH z>bWcKqbJu^Lz;+^Mk!N8j}Pr?_q5OtVhB2huKvGv1#>#a74&xmMz}}Cf~tZ1XyY01y1Q9w{#9Wi zd!46fpZ>%5(MVho}S{$ z=O)0s9ItNsGqU(e$rj5)R-0oMx&AhCC}^t1L&5X3a@7-YzJ%_^{5^JcJHqMRf8<3j zzpsZlphmqb$8C+ybb?{%iWNiU7qUHN8bl|iW{(77lzBx^jTx{a2jXk6}Unw5)vVodq7&VAe<)pnRKrSWl@s8<3NtSyPC0i*+@rI=G!DB^AVJ ztVuP);=@9w`v+5dM<3XME4 z>+3~a>r^Tih(um$?Ao*3fv^)9cBiDtKE--EJB6x&UL$ZSaaVXK*fT2!8VfeP01K&j zHHek+naYq+cyGTxrg&O!rN|PNdF^P?oql8^3JcLpa~fBN1Sqw?V4~4g7pv{quQp3i zi}ti5>sarAg6=Q9RsjK+Me8qLadH4Oqeheo8DX)+h2r%tM@uvQCRWw}{3Ua1u!UXxfSr$~zpSugc$mx=GwZ7yLRNZh-GxeMR@@tJlCZ9^A;y^p{PrmZ!}dt* zeCB5@;1MsQ!Vt7ccyVE-{N{9r5F(Y>W#Y}R?{1ziORV?9y4YX{&P&ukkCp=Sh$L?h z+IX~Gs7teWk|7N`lk@K1jueddp&UBYhF1lIk?MDi3?m<%H$P*&0XPsUz$XM#m)oQ} zI|lk*M%!`Gfc2V7enrNnLjL(#qi@5i%TQA~6JathoJK77Z^Q#=$fz7pba1BZLE|yv;{MRJb167m+0j!p+@=zIcye?5@O&@2*dv!74Q3?Qp9^o?P{*}QQUe67FVg1iveUELZ^%PA-J!OR4$ku2hS?PKAM z5Hkd%sQuZWrW1-tX*E`fcaG}BC1i;1(f8p}ahE)yGy{V*4yRNV(v+0eGhwlHai)DX z4g>cX?o2R@-6pNOdCfG!mk4dFfdNk4(}Y~QeCQH1>)E8E!N@eCE!RX_PqNJ1<)Ir0?UsZiFIr6cY0D@H)%kliBF=lTW1%Sbd7)pa3bdy<(tx%5C61 zul+3ND(zh`iWWspO+KEnK`&?qCqUi}v96~YB@Vw$`&bIU;b!6R<64r-7*3}D!kMni zlWxx*>(*9V-xvqE`Q)o4=2_y9u6Q$b9-+}%@$6w&uJN7OEIn><(NDJCXXCq!9|dMX zWZ{on(RHaOZ_+mVU)0r&)U;#BR=Allg>03m`@y_!k$~hFtVZfU1UCzG`nZA(tphgX zV{squvU`jgG#d`(f)DlgA|oz!?7XZN`hb3Wqe$@xo%2Nw;I;7E^mhP>oFlIldmRav zFNNMN?Do^Hu%Wx}@zo5K|~iG+!itzyRjTyd%?`?*B&pvT|jL12s+mxV^r` zP0=5(?Dgs7M~^l|N>uS->ANufwf$B!M4Q-C4(UK;KZRm`Tc(#Nr7#i83Wa*gq&8Rk z10M1Rf%VAkAcp*TKck2#WuSBL0g}%D@%qs@JM*k_*g<{h=pNvlbMyM+wF~0zKW%S* A(f|Me literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/Hystrix.png b/Greenwich.SR5/images/Hystrix.png new file mode 100644 index 0000000000000000000000000000000000000000..7d4e17ed45e1470a3e04839f9dde212c18679d7a GIT binary patch literal 230655 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

l{il$etinb*nx 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ℜ(&0+kjkcHYkgQ^70~=595;c!w$v`>9jx`dQAAWy(Y|Uz+8A_ zdMy zB)so36w-yJ8)dsAaA>5o4wn1A>njldb9!LmZNFzoY76fRhCMdFz4Qv`J_g|XP)rGk z^!s7u3U}WhiM*bwG;e3d84Wk?Ntgtns}*Q-R4CL`85j1tzWj(Qin%t^SpT9+^p@n{ zPz6iYf;|P`P~dL?-nz+;Lv?0;J_)b$T!aYz%~}BS1eU+6!FW>UEz-t#X|=$14b2*1 ziB45F`TJF>k&+KaV%noOt@kL9^*z=L^QUa;sUq$u2f+nd5r+{2vT;v~@Q7yM%C8nG zY2Z3P#``xMIk1JEN3VFcwa-@As|eEA!@uwe_am2a2%BEb3H>p1s>CYDII{M`sU<>5geJjeyYNWr6}Y>! z$98a$^41>R=T0*p1zTA_BchOwR=!vr={Br%hq5NPH-`5-U+$)rEU9MrAGu02A8F-mzP;9xKL=#@#H z>NOm2KR$ZYz%Wu+FG7y{&Y362>E0s)2?X}d9kfhy)?dUkzbuD#4=(=T5o!SJOn)GR z&XqsX>~%BfzPFSW#sMIFCjLt0*&4gIMJm}+!JiY`emH1tzRd7H1E4xY#pU^8l(>98 zzR5runcTR!KWK6r{H*?h^=?nr_1;29$5cl0+sNnK*qRcuz96afJBmsi{rLgIpLgFe z$4U)^L4x$NaFQGBQafJ*U3%Fno@TmBo3{qz(bwRO7vKO*9CsLS%WdVMXNvlOe2Foy zN=u9W%yLZrut0AZ+9(3r2J)WlWN*aK$;n>MfXW^MX177ndjzt0Busdhf1&LsTewWj z60e#Sy-x|yfC8xUIG$t}L}tRdd;g(wF|MfpcH@bDESIP}8eG!Jfb^d&1PKmAoXK;* z1eY18{_9Sffr=dx5UXAMj3WoWDYlF*5wDCrRjAxsJWBR!-dnQ z0GO!*C6^GEeZlofNe}3|IhgNf|Ct3qsjLt7*Lk460vdhPgQF<`{Y4&NPn5m+V1BPr z==V2kmTw(gYB|$~mSe?VK_XY9lO2*|FPkHSx*_t}^RBE~^Jy35*+sRbebbZmv$OLGPB&Q)>A35P|(70<+%_+T>;6 zAk!g;v^fNYpzn0k(0#RA-FFq(foTUHE-K~W)NA|A3y_87m;Qbo@XUH@6m)gyn`L@} zbkZ>x(-63w1??pw^Ee#Jn?6*Px=+I0%a(9KfcLf|!ifPN+Fch&{=NYGVWeN4po{zC zjxU%Z@N@fWYpC(2reNN_q?iIv#h-D*pyplXnuIUW4@vne(U5TM3X`+g6s+L8n8ZJZ zkN-F*)q|3^%>-SW;N0NEUXu1|o&+$sAF3@nwE*)CW4&r{XYJcTS%j>0d;m4YKwDD- zqOVDlLG<4TsEh)#{}c81p?0dg1D7-1);Rw3%2m7Bvyli>uZl!pU^bEc6|u|-HR!T; zsQ;_PzeYP^_>E`=G(-D&KKN!cZ2|i8O9Tqbz<-3Q)UHlu)lH@=UZChuF*?K4qI5mz zrgbhJLoU&f1_XIm_S>6S`6>3YEk9z@y3FlC~xG|&{LciKHYCPyq{fwC@F@IUw8+~GQ>E8ZI>T8 z->O;+nWlRXDpPm9sZhjSy*Cg-z+&js2s-ARLE=pdlqsd4(KJ^q(kp!Ws7Po9iceEn zcNNLq_q!PAC4J@hW!_Lqcj!k=dnM6W{HggWH*EO6oT~RJG$|d+rf=0$u&$YI;I>sY zaWT4b2TSlV;fc-RcbRS^Cag&q-QicRcXZM6d-;(nh^j}tiuAmz3 z{uI8Eocqdykr~j|XEsAipWhTYjJumBmR~PHdoWKD&oC2WJTOEHf384snB<`T-KdIm z(j|gxtD2DOURKGTC*q=rw$NShGRWs#p%0qc7VtS~R0WJnKvvpiLMI(byN=rsX= zs(yd_k_sBD{Rw+>m3ISO)g&6-P9m5d8BG;1rr+Z_h_zCt0HP2v-%WJ#$G|}4s2OzH zT^Bpg2Q%EAzYbD-FSy%sCl8N$;b%Aoui5d+!FKLw?d5*Y7O1(b2EG69yYyggBIP}x zkiOp|gOK<8!##Z&B|wK=JJGa22c{i$e6%HnloO~zWPFRH<1Gx*u1KM~I;V+fqW%QIApc;4CrxqtaLP+^%qJ?<= zy!-HkFBcDUpJ#75*_M!*xg(PJ!()dR{#>z8$pAPRh@!{^-vF)tu~1GNcCZ$}U=e~R z3V~tG(`*ZFBKKfIl!1-7r8rLAbP}CJJq=86qw>@KsO+L;5<;-q3@XhytcEzWzYpUMvphiZ2^i`zLxmkx81NA8PjkFLsepZ1O zn0Y}TE2n?@tJITVomO^W3Ds?}Gn(6Cl<_1QJVbM_A0x@X4v5|sXnL!@zPfPx)m{SX zUc;vy?!Znl2~MH^IHB_Zi4Y{Op<-+T)yH8(`wiZb^fPwURR4Ifi~%AIXqj2%9T#*# zCmtDO-u88yC`@!Y?or`lsfq;#%}u(iCE68vJv?65?km!*Mbg$oVjdsvVm2@vYHsy2 znn$VTfYWmubw+6sOr;7N{y-G~H3zRg5zYrbAR~6$o;G`<@8P=Bcmma`doj~TAKymU z^@q#pHZ#YjqjId``?-tJqW2_nRJVS(caMC7R_0`T`mMR4S3lp%1aKCx#>0*zJ=M_DLaWY_ni$)g)KUAxvkXp>;|`Uy`|^qnz;%wpE%M z$}IbG+s=>{H9`)SZE*A-e9Bi!-yUAd&<;J(IUSR(Kf5w<#bvr*>AGF*#5f|SD&ea5 z>1Fuq;20%Y?buP%R$#V4JpezCH9UTbx+S zf#_hCWT;l1JTmx$$$z$LG}FK&h#zTd^fR0lC(5!I9uI_0Uke-#id%1tec6sK`J7*q zO2Sq6^mPk@+veAf2IgrnrK2!UNox?zFDni&&73BlP2pDxs})I9(jK&|C{=<#@|!10**HSCjd z38w9506+jfW5ScEd|W!cKXo>TvUG92QQB83F<1dgA%3Lr%0cYk)I&qrx|$Si8}(@5w=W?U4&Qq zJ%)QXLRk9+oex9&LUIP`p~4^c1o5T|;S6jKZn4Fe2Gzz`sQyC zO+;CKy605C;6ZQ8TfW8+EUv!AD$M~|)teMP2dm@F$*-T4gwBjWOrX!2wkox`q<#iw zU~nZpVP@6?CbFNY0{_`98?hk;FVgKdye6j{z|eYk4|bll7YLC4=7A9h6MCDMxq@F5 zTfg=EIJ=G9_{2o&NqMX$=6dg&i;8ihExFdzP{ztk1#^7Ox}2ihF^G!fC_Ql%V6M`l zco{W|!sn9k#H>oZ>0TTDT{;f~t|KrXt9!XWbt4YjqC?+p%UI#l(-&v)AXszpz|k!g zR+Z^&b&Bg%wRwmFcQ&?wSqLSkUW5#}yTUE@nkdjthrHnJWxOLbd^8ojTp<|Oz`Q{? zN&fw#2q8{Or3xV?Jv1i-dnlBO|LsQ!1%OA4N)-Md!HpT2x0C!h75z%gf@ffB+KxJjyc$8pi4myxF z!NjBL>;@70 z8^0GH;Dr7t2>=RLDHuTU&Q&M(6@=;%rx4BtKE0HWf%8=lnpQ)ttU#mqRxm?aSpu6b zVG_sYo4BJLQ0R8|0jb$YzLHC6O!-?dCSa@qa!{rjdl$}aosW=Ix_>`5QyGgC#G;YE zMMYXzq5F6O-v?g)Um7Y-k~Fvmxr_ag+gj(VEV9#EPzmo048rf>TsgJ%m!n4m>B7Q! z63XHrez8%t$G+tta3~81$pF_nIMkkB@GXGUo0U+#C!bwH0Wt|StGpq8S!cWWFy0>i z*aCVZw;}|#W$OGv#8nQWo6gD&$|PmNBQM}L_-Lks;d@w;fYh!k5a7&snL~&v|TTtU!}>aHA{(4!_GbY3Qx1f!!&Iq|)=- zH9%!N61dy)xA^62!m5UFQ(`9oSGl&ecgSr!m7msi!J%6_WNw>3W}t$x9E85VK~Fc} z5Q{M|3(j)7uIg8z8UuUvB=G*M0Q0fG`UZ_pwHcL4=vUQS!E`gyRZma?x&_9Dd;|DG z?cw(a0=?v(r+>xrA}cpC0AA-)z876y;I;jy7MT2oiK0c+IuHwet+|mBwn$*ro?mXs zQ%L@k4JMIjfgbVV7qRqtZXme>l)d_W;A%Vm=>eChtNAYw35LxMg}>77)b-lvhF_%p z2_tUIf&@F1VRNY|b6W!`Jk5Z6>e2QdMDcF{_q)ILalJgsAI%}L0_EE;c z_^JEN>MMwBMv@kWUJ5LZ&=u=emnf?};9t!UGgl>)Nt(kFzCO)9($R}5ZdI9InccFdE zi4R1#4yuQ|42HHY0KcF=dvh}F!V#C}=PHcMzeIJQD(PRakq+c|(=m94#F=^?L^N0_ zDZ5kEpgA$Qm%BZ%E0%$XYX?6)5T2orO9Od}!~1x@umN*>DZ#-#JQ$+S2jVtk9t|J~ zrwMQ*@SXkM4{ov;79|*uU^Du9`@OpCKQXwRC+Z)Kh(jws-~k?8EcQVgs2AP@cn>$< zJX=x5$vSVob43vu=v;G2A8t@`vY9Ar&AA`lmI&=YJi} zqGSMM!;&+7Q|LE5%E1lh28J~%Ist%-jc`2NbCg}Hj52EM^GlYM$8`XzWq+lZW!td# z(>vWCIJC3<0y_=VeNR!C>2Aw1pt%s{gh%^=6gc}06NO?hcmSFvj}RPRT$o*@|3lMC0NxT=2H_OVK%;gj$nWQP zN=8TVKaqu62AL43c0oGcV9_kuRHXJJnDM%~nnr5eH~TvK+lM?s6XQgO_n7eXh}>&; zp5KmA(tqFWZ#d74%<78B85A2F@jY(3*w&Q^p8NH;(ZG%gS>N-_7^6b7fWkuc@v5Ug zK9W9P>2zuHb)9Bz{aAs@_r(eAGlEg)DyAVMh*sT%BtcfO7r?N6Oj7N;nM%B_CC*w3 zG)vQjyP%lx2AGJ7Ny4Ueljfe1#g%;bnc(@s$0b_ybOIxN=m zSVpgq2alFA;;NcTp+VK%3mmAYpoZ?6RP|fP%1&pXDt6}g-<>5T$6ty!Sz=i$7Ll{s z_FyEVJOjqz2MZ8eaJKsq-sH-djhCdVX?1dbB{Ir4SpHC{rY~yZbSaB`Bpy1=&$Z6B zJ^0YQUZRIR7Djjji6=DqBop$nKurBeZtY;ovb6nEt`NEFoX^)9m+aci zSUOem(+~1*yS|na@jGvtsY;kNnJLl{+j0nN!dyHUwAB3h>8-q%yOot6Kg}K_Qc*P5)DD}z^|zuQ+g zBNy)vSN}TChYkSH^fEHEVcZ&%!hpX;z2D$yAPvS{ zFG!oi2!|I{m=jAF>O@T8+hk=1l!QbBbcqtW6t#r1)r6Uomsx17U9!LaECc z0THmhOpd-Hf*=@A@izDX=GS$^5x&=t8@Q9|F2fCm@9;;x42gw9M{oWsbkr;G96(3M z;JPM(sY813>FTWE0t&LRH(%Pj0hBh?!IC4D1!jBW004QmjG(s&K?n>5vCI=H6sVkm zY5*W-<{5AQrRo_-KWD)X*HK4=&zrGD1kHMCgvrXkrpE+nD8eGwFuZBVg4RNP|HWl+4nu^fI)gLc4pl~JJvEti(%z;}PH)tN@D(Q3m_lpu2Rqy>6 z!m$M<+JeY5aZ>>2Xy#LepsS3Qs*%9F9w~H^r11X-L}GhYH=3#%brS;YVTG~^U&!#s zfFwEQxFmR3#`p|aS)U!cR*5u*G}X?7+q_vu7EH)4F9eTl-?ZCADl|66LE{*ZG_0(j z^+FeEx_eoFctsYyqzYxS-ozVcBY|+?qO?1I zDmP#dojiSF!)}qm6?10#eI^s|C&bs}#kyGIE}1($vlXRC6HQIl!6Z}{A*S<_ot>9N z_5G;=JkA5~yq5$h1krE7gQN{00jP+2qHIwMWh# ze-!p=GG}PcUsM*TDw_dLVyW5Yy6|=3zP_>-@Q_#5H6R>jRC=OI^BxkX2aq$hT3MXI zB25kwE+d)ZkIpNR4>{ZVg>T#w^G>jUUwizg9ynQ3mmsY&3DfoUMVA7dwRYwkn#TJA ze^F9>qauSP-hinK+^)NGAUglH4Va$w3rG+4la9jiD235{O{91j9u>6!PJ9-D_6##> zWm!>;dcff_5@{NE1Y9vt4FjTZxtm{-(aP@wMxlNgWt=zR=X$3N!pGJ$cu%>UvK`r0 zEfbqaH!Q{*hz=ti2Qt4VYK*@0M0+5xS@zMuSAsrZan}`0`YmDR`!sA=Px$$0XxpuM zlC-(4<;I51*{$i$CvIzEH`)VHRVg!^85RMkOF?lLg$bw4dANi5bhZynv``4ayy}b% zW~lngs3v_2XIX4F-*_g2476oWC`Xf-D*Ne&3G#O>h}StSjRvBspUxMQ#7ONssi1LE zOZ_EIxX*t8ttBww`{Pgf{u~hAX?StE7)*_j`7_hP6O4x7J~|z4x}+8%n?A7t-WGxM$? zvQfZHP9+(|)Kamzf{BJ)8kGP>v!N19h?+BjLSexr7MgbXghpBBE#8R76K^9?EK>NL zKCj^sIwEBG_K+hcI$5g zkT`9C5MraKAy=EuNI8HPO+^OPVC}%8T8Tsi%%DGPO9QW#gqolj+Kg9Q0L;-;t*t_x z1^G9_1LxI2swioUZAm7aIPDPRn+R97F_0s;pl4`G)23M(iDdB-%5A@A+uAH0NsgHN zVSOkVwHa45A1B9ku+YC8wfU^i_K)y-Y@x2er4w;Z&77^}i}l18xkK3?^Fq_n*A=!Q zN%YDHnk*{Tv*Y=!84{R`wG zBPanDp=3hh7b~P-0mETq4wA4nWb)kKA8(rLeAK)yo149&muv$A$F&%fFNuNvxS4&z zk#m2has4^y;@!;n@E|1jjAhUv>2gTy+xXp7`6!2F2?p9Y_5<-?=N~)3_-)s&t3Xp% zrRSs+mkd;KmJFFfnP*95o`+?gS?0wuEYn)+J-^!b{XDY%WKhXTDxqm_HR6)SPK66l|(bK0Oy`kx->-s4fcI0t>9$?l zH5F;NJ9pnPuWmP8=8^o%mM+^N-4G{!>=Pfy+-^jM2bh-$Ixg!8Ray9}q#zFR(^?i! zd+3ATp_#cK)>q%|JDsw3kn%Si5dd-J&KsT#uZH@;l*@`?oG}11P>h8iMP67Cbo-hd z_#(oKQ_ZFMfskz{w@ z*d=HX*L$D-hXw!pHAujUy7HC$uOufa7I^?4|9*w$st=);3^ho zcV37L{$=41rrEgLMwRQaR_2yxmPswPb=mSkacXp2IGS}Kx{siN&6n6scv4p&%yw~C zDj9hrQ)eNG8b6MN4yrcF{Uf)}u^eBOPQdtyVQ$Hcm#R`C*FRt6RP|B_?f`LXT z=kb(leNcBQzzBDF( z-RwBwhmp9|n6B23$Rpef1$n(M2Kz$d2d$cma|c;zVzZ&Odq4R$HM(eH0X+!S zfK_{<^d{tGA8?Ym`iu8d6>CrqBsX72j@$#pO;<2iXCto#OICBp_f*TA?x1-K4XbRU zvIon0GqrN89pj&cp6j2N&w7yhDeLi4RH5MV?sJE_AJzS7L1{W9^v+-w!Jx3QTr*@k zL#NcD6MF~!W;%nplvMgVK%69DiodRPN=Wov!csK`m9r4AQ$Y7`H#yULJ|EgG=W$ZJ@*bA6uu zbJF9<5C<2$=Sb@yQePBbF4NOCfq;UCO9#8YHZ$LE3yQgQIkP=PU-W6ujHaN? z27Wo;wyQBIuiW%Qk&W$!NAb%GGB@@>>p-Jpaml19qAf zG;xa{Q4ukusP(6DlsJ*#zGH4VT2(0yxzpw^2Ae)`ATtIKBVK%K%v(jRqtH^z1u&g# z+i6z;VT98{m}Dk>0W)v%k-2U2bVEsxaRau<+o4QW&Y_WHt46?ZL6G$FlP9Z51nd&9 zejJz7M2T1uu?=C5adjrk(KwRO)x;jih)>=Lt?oFu(}j{;ddrIy`4)>dXF{dk-SZJw z{c^sGDQ{!2ckq`L1C_Xzmn(Y(aTFcTnZhmVMLg?VEcw$~VxDfWa%R~xPxhT~l8F$* z(Bm)HGy6I2-VO}>DBw3Ri4s36A8j|QTSn+;owHZ@cC}z+b&PGRwKTg zSIMw=PM-ZYZZ-!95t9T{{hBKeP#(6W5$=YkJ;*<3n2%36Nkr>KU^w7F(EL6Se8|2i z%&l&9Oxz7J+`EwBP~RL*w^7(+sOb5QOH0Jp%W*<#6)0%^9>g`~asV$Mzd#U=LL+s1t94ZZ*_I0$18^xeJ&PFQ#U#d*;_NxDvo zuw)L{x`u=jcrd?vd+?S!bKqJ;V*Iw<{V6UM6$=mbA%@!I7g^}S;x>=Er5*fJx*a=< zuSDhuqEW4B_1N3g8-2N}?w=gRCDyKDnzx3TN57(5nXIzsivmxbyXxXYaO*tBGh&K` zByZ;MU?=sRr5Lf3iW>B8PfaUbKLsyH)>zzYz?*(#et-@s?`7lOs(&{(x_xk=^K+0} z#lyVH%~7?S9F!38DVota*d=FAsM+0Bqce8oQy-T}MVHD&quQ6n$WLMdM#L#{S4n-&pML$|+wK*@b~M zR?Y9Pva#NDC46l@FJ$}Y;xxiZ6Ae#%s&4j%S#qROD==*0J1)4%>gT6ZyMd2kd=ve% z;(2L{!BwuPU%xB*6lRFGTknBYSUM|o2-Y>^_q`s!f3F`x?sfE()1%4$@im{{1Ro&s zhzKXL$Ti4r-;hr|Ga&o%Zq|M7h|-I~)|-(>M6R+-dwzGr57Xbhz@Z$p$FG{gXFDbD zIA0@sVFxvWQtHdQIEt#R3QihN(kIcl@2KMEN$APtS0|(c3Nw52TwF#LlxQ1_*4BS^ z>5o{67RL-E$r9ZhM(a|YNL>`KN12~{2f0^@k?sY!3hcSeMDSS2=X(5%JYo=&(^CG+ zRk%U~uZcM%%@#~cd_FV=A8T&QCZ2+>jgvoi6v_WRuK^{IhM5r#DD0Q5J(hiF7lEAo zLytggyOY!K-?or3tLF#nvId>P%X}TlDm;26?eT(l%bL&l`u=rJq_6$s&80REqTQms zw04z}Vubh9wxve_5}{UI7?}s`IsfEYa4vP({F&D_TsUv7xkk5Y*%u;)I_&7-8vRpL zTjDDrK#-_qGdNA+N^_j;DqSTym<8RuIof5JYoKCu)oA3U=>_xFK%g{*3{Up8?*&_2 z&oguo6J0+Ou%B>-5fJR<@f$JThoFT)GD%}Pc%L;d&jT?aW$oOj;7%udGemFHg|y#h z_oyzpT$WDztDxMF_~!|7VFMLf6<`Ycm}K3{c;tWX{=-dnwR6+qRqhKS5vu-TZ9f!z zxd)5;APRkc(i8h}d)MIx*Gv=3m7B%)hxKqmTvgB0$TRQfSO}eaVVBUKJUZ~+#?TRU zGnUyxd~l=S&Ejug`g+>({#mDkK`7Mkq0JYge;if7H?~ z3@kp(0@Wx>E`3HMdOO64UEs*?CnO>{>|-A>wCWjnU5me8*UtXyqQHq2PYDP5tY8o9 z_k=IMkphGX82t`{0HI-!R@*OyLX(xT8bPorb^%QC=30}G477@GqBy@o%0Yojt@WmR zp;M;6JDRie%Q*C_Q~U%$uAcXMFm|tI1Vu^^f8ktJnz&VUEkpB~VO#O{y~4G}?sU$J z9ksVngfj7sZ4JTs>fC{K)~=zttX=eB1B+_$z2eq1ujo$;!y3ytxtrK&f9a2EEyGsD ziUBl#6{QbeC=mPmJhT1fptf-Rg( z_VbA;>{P%^DfGb(6G~~pk=*2EGW=ENa<7K{Vru70*twjq|KhX45?hhWi1{IG^2^$@)Jv)T zwaO-E@7v5OF}Ad7D9O(qqqA3h)EX#k6e_=8^Lkzb2Lc-ABU4TX;6ge;1)5ACdU7Gx zay?+;B~x$3KuEy1ywB+aE~^~la~}blYX~!Ms&AW2zP$`-s1G}iE+r6m(A6rl7#8X0 zmK{mHNT*C#KT77p6~xx)Q>g2ed)YcYh%HXu@lyM_PMQ9mUa+o7z3WU%A-=gd-pr6a zkk?3*SXC*0EU8wB)DYEf>st7&Mlo_9hrQierVV_vsL2<9q^gkEo4ZL$?j!hFF)W6k zA=$2~D2tRYyrsUgn2_N(`1%3wPu#Xmh?UFd>)Ab5R4KmvEW!?0TPyO@6B z=dZiGQSL@GuC^Wt87UsRwXxJ|d(4ZNf$QcqDcVe=mcU@H#SNR=yAdV(nhwuv`!SyO zAMb6R-}g58@6gZT31or+gnl)^f;#N6J2?7uy74>POQh3O4#igdi9|@XdVy^fIk+@x z^n9&FXb>EC&!yiC46KH!F+6}>spNxJLl?|-_4lA}A{Zlv^G8OOjX=jPI>r4GrKrP{ z=((tfS>u~anT1QF5az|aCZU?b$(%xz(4JoUR&-NULB7G<0^MX`EoqU^${Cr~Xs!8l z?7UW~{kGcH(*^*{KNQ;(NmdXJJ0=;m?JO;=(D|2RvgZqI(U1()O2ipzA6O-ZVh8hr z?A|<+0!B*M2plU#O%K$?P;0-e*o`6Fy?; z;5u{%pHs)=@>w8l9VpkO`v{~zqwQ+~B&`mh8Yq7%ckV^x3+#xvL+%IYx9fsyk}~CM zAXE2&lXrZrX*4I+L!>nEB&88H`k=J?nVf2(vnZ4!AWE=ajyLGPg|^Nv>#wc!8|Zm_ zctB3l?IC-`vx;-*&6lF-+tNjD7>6#Is=W&w-HjSz6@7Tt;`h0eemLDRnzTi?KxO?K zKMi)V9OYXZRN2@n)BX;t(62w8f(SIqF6~8A@t4yLO3TFUJ8!c6^!ADqGkmeqvzdE_ zX7KuCPTu1IMoYKNxyMk1<)LTZ*h-v;#1{J-GIMoRuWdU$2{I@|QUHTm)kcAiYweph zdDU(kYg(JLZ$36IE1iw+CSHQvR$o;#W4J-kedNZ1C{Aesa$?cX(?*+wOsrnH9vksD zq3;Y3=9naupt{g_J#PPZq4w|U!n$Ei(TPf|{CnEL-+ZXBzsT$_jP+<479tCO*A5w$ z1Ex;}fMWlOJkoe6^*c<+G`e{E&9j-;w9Mkp1qg3c*p}yB2kFGGUz=0btEnHB;!SuKbFu^;%dX5 zhRr3VRlRI9c)#KL@xr?`Isz{?7Cm!_w?Tug+qNxLeyc9Uqi?03x9^UdVG-dhmi~?h zYnxEb5;LLjuDJT$qar)1y#)?k-voL#c03G=McIfgoD6$3sV_?wd|Vc{#6$4xNNKl} zz=`jx7ruCKE>Iqn7Dnh9;fS1bE=6`|6_<(pI;;A1|3~I!tMk~t^yK)xiGau{6Z06G(3Pr5P zM@6N>p6c4i8a3>&I#Cgw-1@n5d9O3*dY`2Zyt2%zQE5w^rCXV^_WW+cW!5Wc5ma}2 zd5f$>i>9YDjk)KG3PhZ&RA`Hyjy?)P^V)7NU<%WoXa8!QvrF1b*{7CCIqxGhG&r$1 zs=qfy!hVVY=>+C2cLv9aJd;avh(Z?7sQ(OEf(m-CC@$m#;#$ss1(u->GpX!G+RKE-8iWUH4@DwosW9<2;FAPZG zW@SSt{@@R)137n)n_Q7e`)C{-7!dN8e+H&0(Eq%jE?gjZO8Ej0UhBx_8)r_O*y^+M zZ71^;)&W#}eRS^F-zueP8S^Qp3ANh^>`x+QA@e^TgHzyjK2_v8@J*K`?1U#0hluNG zlKn6)^fCOEJb8=A3@8xa6kL7$OB)nYfgtUw-Co1!f^5mw_Um;@_tfm0GB3AQ#y6o; zM4s6>fn_GH9A4;33pGC=-sK&6IJa^$7MZtF1ed9{BTv=VqlMAxYLE(ZM9mCW07A0~ z)n6AKkKQb6|0yj!qw1ZW+|PiQf4w&B(=rLuIO+hZISjXlwmM~yl<%3d4}e1>4q;62 z(T~?sG!X8LA)&~6vDkMtOa(3{c_bUYZcrM@0H9r{ZPmrLxqHq$y;_RM$ zEfnixTl))e=K0L5MUZy#M(U@jTUQetJ_y@=LT1^V5f?m>aOlsYUu>!gz1ON!Svnem zK>F3oiAJD0AST#8L|4$&qFzo=)J?y=!}Gi`2db-5r^~E^&>w#Y2%$sU$(F~Q>_2YA zVD sQ*Uxyb1xeM=G3rbtGLjpO%k#Q89MGo2PSDfm9?6@dL4qv*o6T{v*?LoLqvO zLPQzMF)(alYGp-hgzrDoIs*UL`F}>S>7*0nX0mItT;B%tAKzS1@hdByXK+Y6=HYVjLBc1l5 zf)8fT7LN@BPc8yC24=!vn|R8bm-5h>?6hL6-i>^rYYhrlC$%oj-n+9$;Zwag%{vnl zg?niSpeaA<50ArK4oCNYO;Z+}m||=t6kWuSYJ9OwD_cZb&mT%5V1?1{49 z5S$N1mFH=24?6Vz`Q_E1bcmL`l8ZBwbJT{WshZZJ5@jUj$w1 z(1o_-n6~;gvm5GEWR2j=Q6Xj?JzT!OYfb5qHA>B;>bjk3Do=ezUP|&VNPL7%2Y!ya za-X)SbAayr;|85Sx+k*f9uaEtBSrvv?^=EqBdXTUGXo>i&{KEi*&a_V7N_G@%i{>x zF$v*=wq%RN1dk@}Vr`hfr8f?Q#7+P-?7Pf;lxSi#C~@A$X3X@;ty?8_pBDFVr{08) znH7+P9pF^x!c$SbFtpS76jE@5I7%X-5k|4knzpZr;v7(U3qYFmK}F=H^qit7P;)C% z-c-3mTj0Rx$sPBgcQTN%#xwsF6Q$l>Ap$a+c*78=rGBP1(EkLE&C}WTk37o+RmE)3 zYK;Qxg7_*)CheTD^8yS_UCR^s={>&>@S2YV6QArYJ61QH9Wkgbkx9G+N1XLzRFw%3 zOdflSg+2*^B}F8T95kS*`=027Akc=jff4x|*}DG9dh$BM#Yl`gJd zlogyQTp2Ae*D(F-6Et1~fg@kcfebemCI1F2!-dR%Sau(AcwnJBZ z`{AW;SI6d0CSjj|9l_)H4tyCT^{o|=ClG;(it%j9E7#{7@~g+_Qc{jmU1m3=M+OWp zeLF;BzoGD)Gr`CRQqvGTK#Zm^-+`t{<}4XMlF=pmWeuGT0_K98aDNy^tuuw^h!2tv z*)?q3gKocABNl#0F_txpN>-+!MC~&55qYkRpQ6IC)Koj?Cr7U@O`nl)EdgLPdIu(X zo*nFWgSTBy{#}x|8tO0^EEMZ-C~Fxcm37?fO}E|@%V)*c4!ChM%xQd5`U<-TZ+-Zy z1E(dXxHI+E-Q7Sg-VX#MX-13Xu(Bljj=-$XZ`@H$v~LWbufSC8AI-bz7Q&}``-~sK zR`IqhVpMbq%-M1Wtv?NqIdO0MYs7VDFtr-#1-u^`SFxzib>XbW}h|YI(%-?CRi;SmKOS|W*8I&SM@tj6`Z7E?L0py&X zz`G=5tGmY-c9|b;bM&6sQLS}7X3z7FiHmWgqIOLp#l~*Dyx>@^xaF5)LItYhY#x9y2B=aXqlXQ07jKJ$KNV} z5%ZMX-{N0R1Xy3ar|1onOuKU&F?s}=NNMugdBy?JksVsk*S*rj)+biA zTCS347{sG9l3;#pC%}Wu7}aTL9ft(_CN!xSUNe8*V9 z#6tpn0!~9~x$lID!fj;T->Z+wDOO}cT7hfIvi(~3Xx8-X(w6UY>>cmfjx4MZD z{NrHn;UL=en8sBoE#4t^u%-$cjKCAC9kMo#kuHOobHhm}2R+M=cfYHpF)07p-u5pq zfQm6rYFhkq&!00JhLhc9b>KsJW}QcFiIV5sEg4s{_C!yO+x1f+Dn33- zt)aXTt;oY6QdiD4CCR@|;JhAn@<0rIhsu1C7v0=mpbya9OBZE@ecyTO^y^2MzT`Yu za7dTU(D=JNGr6$VJEx0o1tlLiB8yy;oE21+q}bf-2Yv{UCZ@apius~JHL|8uYH{_! z*=<0FVnmR8D)eIbg|)@AGKWc2CQu;1Qv2+1JK){Wx=@V#qrku=8>2MVVWSlgf-O!BDn0&`oiewv|tuNqv z6t{JmWMvA(GO`&dXbW$|rk@${dZ-o@pU}^c>)!9-0d^w8A=nrJ(}Agmh^6FEi9DFb z9K)D|P}3cnuhw01PR(aFjKxuK%}Ad=2wAc{yWAd)G2QK;g#l$0wREd{;h>zu+O*+g zU`1N58mKrUmIVgQlp!u8b`x29Q!6t zoxR3`sZP?0r?J?;==-#HY_u|>(Ig&TWvzIz@FZJkFt_@|l43wt9;sm}?4o*`g4QtM z23cpoD|Q-fnn;<70DZ+C18sv=~uC}Y3NZ$Cf0`7b1 zt7Rgf+^c&V%m3II%AA}(KMK;IAWeU!7#~Iv4F++1v@z9qIT?%ra|O0YiGfv6s7yJc@ULKq9>%J*My$_ zumA| z+(pjJoDTdzh|#5L+VNT1f>|7ex@J-4@u1fdm6d^v$?CfgMGk3|5M?_KJ_(KzlJ;Jn zVX9Jr($x{0kTeYmU+>i*xUKA7y8%-F3yvcM$h=EVvfHM+0rgvSP0==Q2*#P17Bxw# zxaTVn?U&DoEy0uZ3x5Ywm*?<~1**d#uSuah63GfL`ax%QNm)2%dlA0aEIs9PjWIcs zTvq1Zr406qN9v{LUL80z6yObrSF^F)2($hAGH_s?OPm13;mZ49C*h@c!M32-)*-bT zi$};v%XAHHFZJ8rDXOS;UyfjB@l~!+CA*49QLkh}v7+*(XNbv6%l6VEa-t^Y`XHW2cg0qVxI9J^C$u}7k4e75- zu`C_&iKlIs$)mfAa@%}hDq7`1BEpWRyN7}l8|@D)sjhgD_8jYYvC{%i-soyqY|ng% zw=;?HKErSPTDRbx#yx^jm-X|dzj_a10Vf=9ert9>v>#`FnAiJk0hkvYwXkaVaGCtw z%mPTRmw@(hP1rzU^~cF!u}A@PPK`4Axrs>j5nYfw7$NXuF!aUgu4q5h(tGYJny~_| z3j?o@UmP;`vCJuD`;sN$&V&dV4PH>>Cg5uFbrbj=`-z3|-}X2uM*c4HY66KOmuh5` zf;VCIEXgsQuy;yaD$74Z<_SP(gvUfi-EXPEthRY9mr^{J_oMUIl2i?Ap@NUa!xK z>ykUO$=06tgGw5rXznRpVm&Bw?(Nfb&8#SmtxF#yhjHfCjWx?C;yQcU7B14zBF!g@ zab?FbW8vO+$Q>QihB_;vE8Tb*fgq)Fz9Y}%4sXLd>JN2|cO#i;&fSSJJl^)e>GSua zl^vr2NUlPN5BvU;dEIfMSJ$R6BQxEC!ci9aT<1@YGYTuh)PRAwk~oLHIgYq8J}*6Y$N$Km8%;4l3U z0wz%|ppG1c|F*w!M^_`YVmP#1QI}%+d{_qAr7@U4v7p4*5Mu#~-V+Y85~uFx=$&cG z+8YUF^Ekhh7nXmIJAH@VK7t#Y8{~3?R#uA0Y^->P{m3=P!f$%_+FhgLZxi~pMs5e6 zWo=F{V`9G&_LF9PxM!5&YWVk*^y-r)p3;+_p5G7ipd&0vd)}o>$qy0(ID*WKzuB~(Eb|9Ce06Z6NJZ_eo)Gg{jZCLsH;@IRrVo<2ZO zy>zSg&_7|OzyJ5Um-v2rg#6>D$;&4yerFeZC3WU_SeLr6nz|WqpVU=uP8Zz%>&>*8 zJ)%>Lh-wI*2aktrICJ;Q9jtTwqyA!OA6yCt-R3|GHSD~Q-3!`dDUtBE$J;}L(FQIp39dMWv=y91 z>DEM0Hr!_*e7nfops&#aW^=A-1#4W?%cYovxLpZ8)S<4|N609lNTHW`d!1sBz=is# zq$!aW%j}dO8Pbfw#k5th+95l~aWT+R*n<>!k+x;+-n3ojHpG_6AFd%!s-lYMlo;ND z(Xj?wVtkjuOL!ZVgFfgp0#AN`X{WAG#>t~!4{{FOgE=45IY3tbd=DlIKEe;tDoc5E z!;t1q8AY;pvEnX88elLloIk7hQs>Hn=T*>6QKH(V^w_FpUMyS4v12z+aGE)TuvVXL z%=dhCR&S{W)0lF|E#oKT((Ef(rf=o3zpmhqaW#=J#m#nX;s8?Oh3(zw*GdI*8I72GYe)hSw9U9w!L4is6J&3O=B2W&K&+|* z63F{6Ofl+CK)HL|212hh<-7|F@rot5MlRvtuuHQD<_CB#eUm-_x-#s?h`8wL_XSv= zx5xWHzf4##B;_FpAtL5tfZSb!QSarrub}(hOT=nW^dpO}mrUli=9A)K2LZP>`?DWm z5}9IS!#CH~2BWmDRIqX3C9k-~)i_$6#~il*EIo&yEV{vuYI2uY^IL?6ZA)04!W#C6 znq=aHd-moJfOQ>XOq+=$D#GnhppUUac&^fsD?r2nb=SKu*5}4!CET39V@3}3?*Z@} zLjY__f%~4zw$^8DBlyB1WDe5(6VF`X$vx`*`XuL`z%** z@m2-wsGK%rLcKEEXFqTSddO#7e=I@<1G-5Xj?4A~;{Zt&Plkx{L>V@c_c+3NtLv;_ z*nZN!Wh`PN%%~N{?6XyxZQs&`3}8=se|QQK1;i z;AEg~YoftZ(x@YdO}u}^=S2G>954Z_5L|3fcm&bwOK|Ump1s}(XxcR0tLgPV<`k1? zy9eC!%n+`vJ2aly0di>6b+DkhdT+_bBWP*hzFV}uL(HA%wb9YN86oSd_f;&B(%PeE z*+7wUsciGRmZ95pBbNm9TSY-Fsa!sV2-=J5v(F7V4n3D!bhBszTJ01qi_WWe-gAiM z0kQGSDzq5J9ir(mPX}B)KIvUI029d2>spk)Kve!oOMx^g+4+Ao30SiJyr~@~!*L}& z&h-#763AgP@gx)OOycm)|Bao!J#LO*d6uSty`C&8hCsX=qf%wo%5^Ei(C$HF0knEV zrV|=pED!Z_`iQZjBBgftV2xv$$^PdON0x56on=*?Z@iBPQ2}r(v=KtO8ER92R!V=h ze*tFe=Oh)Qy!bHFVUodlg^0?qGx3A}S$l;b8 z8p`EINC?7IEKdOHMl4+e(9JR8wp!fHnc23q+0UlJ<#Yg8Uj$Xd3wFjl>F`8%7wMq3 z)v7Czbj*9Mm8Sdwmo)CXlZW7y(j-#sagq@-x7q0m%$Bt-#JTUxecFQs6Nqvq8GVg$3+3zZE=J;$I`AprSh2hw zjIXpkHBAWJj>~^TOpl;(hvQAg4HU(bUcmkXtSXOsy z_Ox@w?#m7kB7Eyy6N_DcyAJqtq{|wU?W8%8C}f zh^=8Zesp{KBzcSp4!)ZJwoN62`%;QCZu$VheI|^AY-!631*@{`Q(+oUfvV}Kx53N}a9eqwE?U2yX!{+WX0GP(DG z&9dbdJLGzap6qV{JAD`k*pqViqv6650;hE3f7wHUje*BZ#i|F{`c2t#d||So=%-5k zN$1bQ9Y_JEdt|dYZ_9DdVqCkCEdEiuOck+YM-pilwtZ5GpQnr2prPpD-Cc!l7tggn zb?mttr-Db$?-Fny#yHOxuyEV)*)r_`;pJ4O1_c1LFB(I({K4$^3n+94jcw%^`Ng-+ z-ET_KP^&qaKoo-sX!+naTE`j?TpVFQq5kK~SoiJ38)L5y?a3aZni=udwW;h0#=yO{ z-27zDK9Ete>3o4Zt%*fgLplQ3Z7+wl`Tq=}uPeg=qWpf8Hgd)uVs8wI%!_2u_P#CY z*wKG48CcmpkpLqDwe{=!30~zeZ!H1u>?cO=`w$$+oQ^dq{}%qQA;SNEubcas?Ti~>iX`k_7N^nwl!I}2}5?blo!+Eh_CAobHA$lu)T`uNOGv) zTaj#w+^yUk;Of2Cui4Fcz+(ksPJerw` zQnKXbkFck_uhyImKAZgY$4=re+KB45WVT*yuYk&j)Y4xPy*lq*lK+D>F!41DOC_4* zP7_lq;NplM=`SyL#4lr}UJL_F)*9#3R`z&C2*ZeV=HwmW=-^X0%dRS}43TSIE7`&9 zkri)7wAYL#jJeE;)(k_PjlJV}b))qLgut7aMC@*^2f@&i^cX`-{)MesABIG zw~*aOUjn~*ke8sMY4;pGcb%4bjJkL2x^Wq;KD*lv=aw-URjg0Bo*Y=l47F{OFq$Ot z*UlaxIue?9N>DcFarcCI-M8CTWGDD-6oZeGUz>WvG_hc{ibO>5b@WYj@%QL9)8 z7g|?&gKJZnLP90(=Vk^=P&7DUkNdr){F5pY!%8i1irK6WTFJ$w-_sfoYpiJ3xK?I( z^cng0GuTvwfAN1kGv1!kIvP!xkPfGhM7h`T>g#L~fuFf&sKH0lxL7&Ze;Q%2l0Wo9)>}KHe zgM2*}>Dps+A8l5Q?88#PHqUwk!i$himFJiPH?LvU5Vd29&_E^A(Touy3Us5cr-)^K zVQR;uPi~Ar8S?34gjWOAQ?j){bK%pU)l{h~uVVE@$&H8vtj8|nS)I#0FIK2-Yu|k( zPt!dJu0M!TkG|9NJL^7XakAbxs@QK26s)=&KZYHm5K(2NUZs_6j=)vgz4KM?##yIb zARR`Y|KPJ7bE;>Osvmf49H#KRj?ow93BnfJL-7hkrU0xR(SSiGYV_bx+uH8Vz0?k9Pnt(*)R*{7(U0-f54HgS+FoHJP zQl>C%-s!~U_x@Zv23&WOs&9?Dkhjs^P}92C!hNF9l_|kC_!>N0@E5ywoeSRx6<>HS zReO|{x~pN^)S56eS8yZ_SJWEmY{&P#Q)BK4)#y)`xhL_tmlb<>Pi_zy3tcyKwM8w) zcH<1u;Zid0`k_fy!M{o*A+aB>bro+%dzAbLZKD6t@kK7y%n1xg5R%0hTk&V3x7UpF z*|fk?=c9|L!DIs$7L3Bu`3gb~9tfCUz1*A9t_YgJtS?w3S4O$u6V~>VOUTO&%r5&V#K|iT#Cfv6^yk^*}j~l1m(aefb+fEK}S49mYN3!@_Z7Q7RnihT2L| zE~CD#TgGO|t|gQOHR4~oy@^mvi%?hUGc8u7C}Ha~Di5#UF87n@to0C2PXI;%)fKZr zD08rE*1bRS!8krCcQ=`tP)`I(zyNg79~ru?`aqtjWb=*XwGb!w}1VWTNMc7&C8fnB>Na4qCNVowK2qA$L@iSD%aA z;qo16mv_M^jq@edMdr5h`l?uw%)>u;-B*JiUM+To3PT4xK)I&q75$hNX_&qMK+ZgHyQ$~Ez^Ra`)xx4Q3aY<2l#%{}-o|MW4+2`MSk(iuJCM35d>VhJ3*W7rD zA7*S)%{uz;IvE+7E@S$ufA29D({F^KH`si<)!0c-GdNwzPScOg-AQ>Kn)^o!-}-7- z7;lFy52HbmoxqJ>rhULr@rU%@hw=HG6d~qotJWkUfh&EsE+3jqMBUEISqd>5&va<+ zZ(vxJMO=C`wPL&|4Ak!tLBv*!m|eYdgBz(r8R1cuN%=5I2w8O3PcL-oT8~teBKkRp zjUFxkkkoDZ^*^+10t{^%SgntsfLgV?VIlF#SmDKjE5CTNYTl%Ys9_alk{IlC1VU*a zzs9)2h=V&dOUT+2TTp(A@7nuLK zGm0A9l+o$^F%`?V1GcQsYhNMUCik@s<{-*1 zlO%ogwn!LX!qOO?R|w6E+;2|Hn8DnOqnIIARF-Yz+rGhy!A$iq{v%WQub3FK{9jeI zLU}GAzNPvOfJE~h4KzQ2MRS_lsV7Y+2<)@xXCAa4qOHtc5|>waQSoT-lM}}j7q9rk zj74YT0l6ARoA;bHW?bY97P_6cltom@`;9;Q6EoB!ab*s%7#Yu;Es{p~{OQ8y45OHV zmzu(9uE;fGsO2|WJ){@Pfj2GWP=w!b8XdO(s7H*E0953g+|!0Nyq0T!N3K+I;j~(# zog{DdWmi-ax5c9XDH-K-J0-GXQhyxykAG0!desm`H6_#Wzx<2^VA%ifA0s8>smBo; z@W;U;EU*64A5zLky0ZejZI#F}FD5FuUXRI9hi*;w<%n5}6Aa3XSpkj!hCGO`&9^y4 zX72{vyJ@!SUb1hc2~SHYl|Qduh-Hz_a3}0o=S?-9r7d(d*^nL1@$g4{$$=&xTxMAl zjoH;(i4HqiuK~%5le5vmmI*xXt8nA1`+@6{zq06uU15@2>}It8x#RM1zl%{QF`3v& z=I%Y(kL`v|R7vl~+*=7^143+esT7xtTi?doUg87jsC+s~cc{a_byao&gp7?B7bKs8 z1>y~@CvD>R0@#WriZs?5_Zxrt9L#f{{pb%z!06)=B25Poh&H(P$ZY>CN+R{Wgwuoo zk%e$Nx6?dEoG|@>S!9Fs)IEVNDT-!^QG0u9GSmvX2*8YskXqjS2~35)1BV$OB6Ak1 zmq35)AYJ^heGGb51*LYF0jMSQuaSr=ZkyF7uCRoif+v4q)FPj{M@)V4WpjR6Hf$bd|TjQH@rz8m#Fx=WE8W-(=pAuP3{Y`_d19JM85}0kC)x-CfML z0=CGfhbmXuw)I?BeK|B?z}0frhH_w;|V50^{TT?!{ZW zxsGK2YAZ;{Hk%BIonTsclCq4m{$t`zP3J$h-q|#*)pOx?W1-Abf-LAz=CnlgpQvp@$f_qi{;1EY(R8zsAFa;!UKN1Z~uLXAX|UMA)g95A>ui*h0-a2$KFG)W}&% zB4Nc}ay=_uBQ0cZ^yJ2Ej;KUW51c1?Roj81bjRyG;TLCQ(ONyWM`ZO6+<&(1@M2Q& z0sPuMnSkO|Hwz%&GI@~er9ULUmM^4I;vM-7S3%@0UNgraLDmSUT}_D|I<vyN}YPV%Fpg_d)>tBdzl7x(nr9m`|7NACNrcVS|kXOGM4nC*dDfRLEVj7+=+5ky&tR<+W3+(QVyTD(wqp4LDUAk5dM(8&WP*YnaSHQKkJx%2_UP~i+GwODt2d3y`o2dhuq#l)=brk9(JIl*89=wZnW2~lLg9kun{SH+p5cY6R)O=T(5cDMm>10J-uEmuf0xhb2sBa5%rz z&O?j06b{7SQrI;Q@vJ5+*({5s`}?yU>eVF$bV3LjL)7{y*h+ezySZc=@B7Tt z;`N@Xh;Gbv)sg#DoDZQT>tr6qOb{j5w==_orI4N7^{Ht-WT=e`XDgX&rp_VkilUlU z>}ZvtgM~(Eg^pGSqJ4LrWFur6#&`ABv;w7MS_}{AgnfF&^d{licZyFG(ZDY@1`j0l z^iN>b{ezrholl2GqB1>SOMbG0>-7rA`oB|iPl{g=hg3~pN@}tX_|gH(=S!IGaXn02 z2eYZt;;BmK5HXxQK5Pe4W?PzurpCq!^;lu9+2UhEvrgq>%=QFz;J6)bJ86C%8#i1jn+?U;P~t5F^2tH{ zMx%35TFOQfHtz_Jd}6}knyVY$G(<2TepakJU76zF;he8SBMS$1sQrt)vo{gJgJoe_ z$n`FtmI1AsWwXIc;L_&OQ=%&Vs^{i{u|n--Cuy?W=HqgV<8+n9KP&fS2of`*`?xIrM;w_voM;P49KgE4I`3 z;}y@2$fK`HBu*_ww5ZyoCn0RYPYHah3hwjd$tj2NYE5-$Om@=~+*E_L=7C7S^n0=q zn`UcmKG`)Ob~Lp6Ox1XVt`CQ*x}s_?{M>T;i|UJ?)CSafTBCd#*I~i9&+J#WdC7K? zs%#M=D6E|YbJ2w`b6B?+I^}8!B#roe$^IW{1R6;rbmAsWl%z->lZr__7o}{hU2NhP z{+KuCb6C{Iv-C4Cv2FeO!X(D^KGEPPCE4t_`ARONLe?LcN-{q;n(Yx0Z|Pvu+{d$? z1%85^Q#H<~KX90DohIj+&;VX;wrN|!r}K{XOuh-%s;Z)|rD$YM4f}YTz|5Sf&~aV{ zY_RJ)HNI~ykEQJHnstsnq?CAx$N3*UGvUX#15Lttze;uN3FW=d6dz9&)W7Jwt11a> z^I_w`*es(OFLL*jh%qVp;i9e=$+Z6@3tuxn6hG}FW^`s)UEwmY^pHC@s zWSxH&iLl6pOr=-2EvI>P3gnsh^ASSnJ2Fyxn7n}n5FpLBk|6vJ63G4g)Xs<-2dg^& z!6ezN`sEFiMV;hlYHQv|6Y*5X0!p5J6b-kofFOzT8UYcuh zZZW)SKYN$-#i9wx4zZ-!JMR@vl_vlNLRGt#Nq2 zTvP2mh^*6FWSy*Q>B#apVVypdX=Z&Gt!|9}pFg5;QuoNEZXc1l=JZ#7pF0K#&Vzf4 z`E3rKhr~bkc)L-%MRGWKl2b?60kg1VJZPfH{Pjg~sA3%zJ4Ly3$Vpk=`{2r=1iACk|0VX9g#zwRLa%Ate*n`&=O+jp!nu z3vjScY#!?nI~&{mK>XyjxfeMRRjR;ajW8$r!BDYsravezn0H#c9lm}mM7`L0c*qn1 z+^!MF&+(qFP)Lr|DZH~H`U_urUtdRstql$#dXIbppNq3Rwtohvx#jgB&dSnPwVhI~ zmu8`dh++3JZs#;)`;5k2>{q^{unTY>P{qf%JP1((dTU6HBo6{pDiBF_!$%3Go^K7vi`~?ebrU zqlQgn?2$_^Zyq)``?NJ4(=C`AEOIzCeR^2sIqAKSO#1i|TQe9u2sjO0^d^XG5fmfF1A{V>%BVy(%91$QUp0XhZL&`~J z+eF_Z@PpycD;?UTY1SZ1Kp~&&+q4Q+L+C;;ZU8`0=&=tjH-tca8lyDt3Msx@U!asA z>=ILrVq6(SFe}>saaE)AY+4q(D)fpICeN=J9taNifbuk!>HWMaY5aubviq?+wM^zo z(-|6R2>s3L!%&`|LZ?rQY20(a>reaImFGWGBgiA)m9~B8kqzC#>V7(b@)WxBsIoIM z>Tt(Y2%IbM#|*_Ktjm`KB@NM>9fMo$p4aFrO0O1_=X^LQfwZa1dGIjBEd74v`G|#- zZ<;`r(;FNCqHp{7Ded-K_48+*0aK>zV?k1s*8juaTgGLTwtb_DASKc=(x|k6K`U^f z2vP#ljS5INNJytaC?L|Hh;(;~s7QB9Dc#-i9?O||_x%pnJkPto><|0*e7WbI5xLg2 z*13-JJpRW&e0LAEt-q2l3T9}PcF^3eAtTsB&~eRuYh{!u?M6X+H5aHVp6pH2?GjSE zMwr&_(;Q0PQ5)T(KKbGuj=mb4SN%jUsvz|==l3gN4;42xTQm$d_V-2#=RBM6tVCKf z9(N6FA4qeY`wdYdH+6$2ATNB$mMn8Vf`cyj(q7$992|XF6yZ%C&jrvz4P9M#aL(Ba zy;kY}-Wgxh@{8&8MJ(d=1Q%HEoH+^R&eLhnqIpg(0&+V&+nH1^6?}6w05TlgxIMc- z4Te~`ZTKg(Yc5Bj1ju#p%?Ia`?c$(t#=M_zik8e6CGnedJ<^Zouf)xoOP`qnTG93@@2!yBLjKDD(r z?)4i1p8E~}!rsSx-z8f?lX>sI&JQ_dJj;6#T33GKYBZ61PBuq&Ma!=DBsgyD5#9YB z#9nBq@Cl#jOSO8|;yvdzQPQo=rIG4e`0G{$ztPK{KhaA~LV%zdW!(_;veZAL!SpX^ zGTA@3CND}vl+bIVXooRE>nE)qAT0yw-ozIEE)&uZ74ppc)8nFuSz}PUHkV>DkTAHdUoR=Z*+L?@BfB6iAQvo<pozYR z!_E6|A2#aNNgeK}m;e4r5Rv{HxI~XWBY#ic1HLuh|16o~EB*hMOC~|*WKraw@3sD2 zJwTM*WI?N#+tG2~-4&RvD&T?Xu8q&g$Ox9EwemXqQe}Hzdus*JEO(R}4!pa;b0~JC zWW?Y%#op*e+Dy8wklSZ-%NNTU$>b5$0wQTY`cfiYh#VD&e%OOE=% zBHU%>LqtYKMpNsflV}P>!WGOv22!qMk!Fg)n|qCuXk-wD-3O8GH(G%3N=j=9s>@B_ z$TE-d8r+NFzdFOqi*B)#CByslxx%7=5w%FF22Pj9C$J6J%CJyo+k!EHWJ(9vUb;U^HX`oNMQ6S&FX-);-BO7@=*D#m=fq(4#l-L2CB zNZMEv@kfI}f#ekXGTRXNWO1nFe6lm6QLO8-{GPknDs2FD$m*jvugiCYU^7-{%d(c~cfV}gD@U^U56+83SAMBpK z?!F^@{81Ar)9)z0Na0bSg&UMIfdN{T$OD)-7s1See=IT%;46prI~3|Lxnegv0#Vic zm_J<+1VpR$^^`xc1b6=cSXzxE2#VVWX5TYCJT)tzu(p&F+6arz>El`-@r|~u>c1zn zp(M?Hxg8RhO<{zrz0c`@FjztCs?1QGb{-HX9hMLnJ^+*1LHWs6;~o3-T|>9dHOzkKKhJ-H-{y)&Y{7aLg&e99i4GTRHJ&=Jz8|ap!}Wv0!e|W~sqW9r%rvhfgzNR?5otsSqzzo^ zVUvzlM6Ls209T3uaS-;0uy=y;%01xXMwc28rRQ>2I;6SO)zu?) zh?)zm5tb!mr*D9;YyaBJ_V$hZ z9nb&aMI~J;;Vd zth9|xu=?{KmmgFlzWgCYez;OxebBt#HYg1OwqP=`{X} zxv-}s<%t*+r0N)l=0Xej!`mqi$p!58T6Bu_Gh(xwLhV$qI?NX@xk|7HY^gK8Ck^-4VyKkGyMx{TC*ihL) zNFr3mB^QPCECKXt?DV!Gpj!u$`5=tFmR8AFwt>%m;97yz?755(NHhy|K{CLZFj~2i zEk`34%938}@ok-uDR8M8yr4@j%Mv~9QGZ-I=Kr%1YYUe%-T#XyHqAI~oXZYgfR^*j zha&Lw?!0nSXSVZWaP(4*X>`PrYx0~8q)j_2m$atoU}MpHrKar}$3v*t+ZKtDld@4y zAsYZQJ&owHdYN77-1$|g8Ga2jWiE$NN$OLp&fq8VmxLaRSXG|IVoIz4#YWGH7kMAYrQ{TC?X% zMNN_(3Ltl_9lfeI-j&P+Stq_h<;uDvGSi0y!ufi5>(X0@)-oX^Y9w40o1jaD;49h4 zFyQ|K%@jnczSU7#jh}t-4+JZG)3mU&&6e}i_OPa6^w6j--xC;xFfLlKESq8C z3sZPUN)28IGjgdQd))LF*{>0@JA!4zPUa4t2khftA3J*b=~X%NV}=fNft1Jn{;$!Z z?Sl=X!8t7A%O6o<%AL@3%MezRKgHP;Zu4ouV{w!uJ)5pQOiirgwtQVX0x+Bp_B0)p zZYKGDeD_=NWqoLFwvO(6;mTIay`ZEb`p1USGdYl9yll0Zo7ugT@e&O|g8mdn)2on^ zVrfXt)gqxH&NsEwJ7n=7~Rz{9!4n5x6TKj5CW^yE{ie1)m#>hZBfLOrrR7 zV*a;MFbHvB(Y<}B+M%ZrC{GE2cNPkV6_ z&C5WFv3~cv&joupqGX-}O1R&~(#Qznp0bsS`y579KT5WuxK#;5Az^4V#B-9MO&(Q~ zguNKGt!kNqeFWicrp{~H?onR^6oP&9gu*#YKN|!#E-?yyKOFTYtWe&XA?PP<$oTxc z#d!3Sr(3-qk*Fw^fbyFT`$$mnXdnfS!ku=u8_6eXKt!cpI0JD;56w2#jUaK;XbbL> zfRairBV+Wmpmu!QNp*A^E(?)cLwL>eR6OL6!GYn7%323_s8ntPB_Di>%lORx2k8js z^hHHd(z%6$QT@uHe?|EK_HB`~TJnminaH1`*R=nsf&?bt z%5Q?|dC3bG@BTE5oi@UdxCEu-G|%JWSyp z+iiFqSQ7kyQNFoL=pDZ9cf|TJ=I0^2kSY&KE(n$MP|d)p+X3X({=+xpLAMtR)L6;L z!zuT}j{}~V%2_(6S|J)~nja)8>PA9Ct3Kvkinsdx&IfO35m8$Of|F z&ebH@=nRHs?_hZCo&DnqJvWyJ)626=8yx|y(!Y;3EpCD%R2C&Q1b4`8Qdp|jN*JfS zOQ=O`;@ww`v92IKUcDFZ)Lg=Pi=f050d4y@kgmF#W)rWI@U2m%u5K_w4$XoJ#bC_E zrWm}Y&R~xev_fq?K%Xf`CP2ON9HMBO0}2yW z(JVMo<{$!z5oyt9AeP;^{cwj$4GjviiULT7Rtq!g_p+2P$DC}6d;|!*c(~jwa8nAP z-{Iwcsucbtnf`dg5@K+2^6vL*SGs@WS7h-h7%R=gfhW7;8GC>g@XbiYQeK|}^SXav zK?zKl)34-7XQ_XGxdawW?(EOd8V>n1!f|%1wG?v1^@g6cu(_)0yhBC}<=bk#<>zB?|uW)coU9kXHM3RBtS@zJv7&1UsCa*HfB z22BYDA~!1f%p&3=zhW?4I`#p`{kRFA5r-L%wCG}o0ebIt%<9^CY!Kit@i>RwG(v@P}rfzSYAoL1|%;28-- z77ZZjWfT-}TMu?NHGj1RPL~29C+psEE-R>VMK{#=n|AJdY`<&wH3w)d zH@%}DRuWSBY8Fk~=aV1dP8m3v1E(K!(%Fmz7p9jH4?Jj$+pD1b7HEbx{Ce@%(Wye%8S2N&xy8Com>u~B5 z!)E${n?c@aQjG=!JHx0%2$ztjTo+sV4)lxezvE57P&h_XDLm3<|qv7 zT>rpl`XZEc*m`?xRb49@x-r-72N?v)GVdjVc{kVsqHOG`2_A7ft&YpDxh;FsL7M8q zeg!n?{XkokiGS7sk#F=+jqg-($i$9<@?5FDG9OJ z%RNQzoIwQjr1&2CH_U&Ovuq#>DT+ z$78Nw;3%n%b~48MrA_x{#q$`qT?*?>pU6twC}p1<7zi}YR5#9-ce|%@PWB94vbEfG zEJCl;W;erpjPudwqRq7odvavyudltxf84q`Qn6*yEA|CW#7W>MJ!AmrRx=ay{pJqw z&L@r*ibpg)-iy7pQ?G)26C@zq?K&AwzkTn*2v%$_Y?qT;D)KpFu#U5@=)Y>|#uqaZ z4uGt~?Ty^y=NOzWAJk7(A=7t^zY9AHk>{@ctiQ%UwyQ|GBFUkEd2fNMQI=1jCU4)Q zdB1zDNY@q14;HD#1qb;CBKfaN_pAEbl6|HohxVk4E_<2p?3!!;|Y6T;B*TUD`Ai z%jK=vZuYQ`;XoLxyak=in%UZ9gZK4P!Jytxqsw=q=+$p?JF#~A)bG*)Tgyd90qHRc zp=9VjMgz5`AczVNW2-e(>K+^!P43K`7K8q-3Zo@JLChN8ELRYNgPC{un{OKl0fj{n z#-511DtMCM=zS?6n>6yFXK7lcqa4*e&v0>Qo}=Pqlk5n&OF4U2@?<#J{_#ml=Aqex ze!$lEJ(EuYj>j4-ITC|Hcyk#V^N|z_-B*NHQlWI=`meTY9+(gKOqniE{t=LU(M?FT zZY5|CkYmesEGdv2%z%n-qxUe+3kxrXg*9OncKW?R=dHyB%eCw3Xtqm}yKuH3sIgVU zBw`WvG=HOaH}R=qUlC7w=3oIUFX3%1DIPixzLVBg5n{DcSjDCSzdtYkvDvjb6s3So zk}J5*BdxtAXlknOR9=qa85G_rntTqy?v;ZGf<6e1>w(Q>G&nR52NCV3@?_206}T0i z7({(Z&jL=(40hu`WbiUi6d-z&q5behkK0s48dbD+!neu00rA{U%@u0pWL-E-&tOY1 z%Sb46bCF%Uq3;yrv+AvEo(i~nBKC^%AiHzwEUmjiVqv6`9a0~ann*|IM>>NLt*mQW?&1}tn96A#;1|Pca zc$-D^CuFME_UWDr9sV9H=d2fZd4|DRkQHw2K(Y=w;!a<=JDXkv6^ZM{x>K7dwmO7m zXbl4vGrtuje`{%88VpgW3#`2bi?xU z2?Z9Z=G#lrhmE|K^KsXh`x0-^%(l3gn%zyPmUZRU&rvO}%)BRCIX04}o54X@zgT^n z+&mDAxXO7rqt0n?u!MtmY{i`xudvr4K&UB8kuN4HH z#&~62K~IsFg0#|giN+FI_2GwwBDS#Oi6&L+8-r!oVe%UeO$~(nwF?r(@hQ_p@cb+S zs`Maqd~?{0WHN8TPi`ciq2Sbi(~j|Da`)Dhy2Oy;T{3$n6jnXk%ua$q-9B-5(Aa;0 zFo5*jcW1-P^lf_@`PQB&uONYg-eQ0*M8t#{mVooatHuf>w zzf_8{o<&q2?wVHj)nL=jPYSig;Lg^MG7{+8!NIp|hb#yr^i+VC6+>josx$^})#>94 zJcPsZxtu|s{iWGEaNIF@g({vLS_t+Z(V014$YTDWm}b*IimK4Rw3W{1jQP3c4R zk6rU2A0NdM=iSJ7MVY|2{Ht?+XARLltn?QF(_a&m^(?xgK4SJhcwbdW!t1|T{|t^c z(VpT5f@k&jdp8!wnG{s~oOK*;GZl9^64w^I*Ok z4>ZhPPh1)#T?%)w08b*emrK>qEy9!3_7&t2@$y^6Wx^s!yLQqxD~Qxg)1Z;^_1&qo zq&1MV_q8b=$$t2dt9Oqci&-fPf^5RW!*?_bv+83d(G&|_tUm@=qRYAhp)1wV%XV<- zPk&rt(%8e5Z!q^}V6u-$f?$p(?`*T6TBV@VHtFDu*!6?Voo{9f#fF-^-!SlHo1(UO zU&zwOHYQ=iaF6c^DZWcx_uvLxG{lY#G&b7AcP4+upthC_JvS#>_S)+VtI*Q?n9 zg9z=*p>@wpZ)v2@Y7@@~!clGilSAJF>}pPP!sJl_R*^nU2DKmqG%uJYhSPQ!yXHr! zO87>oO-v1Lvj@BEZ@ZF0BM(ik%*gH{mgwv9+Rc%W!0eI(@ZghPK`dfm!miC$Im;j4LDNYAhm>A8<#Dd*e9rZy9xn$2YT)VlRW+s`pM9z&mgx?@jcmd-?lq3Hk zfopfh*mFU<=}|f{!g0k9GBnKHSJU!jg_y}$R^XsN{|Kf@>wsA~GaJ!jW>3`sRo@&D z6^QyH>F%wn51KQHMjkQz3!VaF2ZmDtN}dQ)ASBLXlAj+ul}Q#Wz)jLe6q8&SSUVSm zP_5n8Byo4?6-6<*uDp8?>AV$0z)Bv`)w8)UxK|o={ULa@bDmg%k?;17#x#tJjAGFt z`czQq@)D&W!VHdZ%IDf@m*Lb<+%OR=ILiX>iFSCS3lFJ&vm35!UgJ^t&JQ=w>PRnUU@Sb;%Q$>Yeul zCdk;?*`>?k{jLeZq^v3W27_8oTJ4_F7Jtnnacx={WLHZ=_d*iquZkMPK1)6A$G^Jj z6(6PfGvHznj$tku5{ABP!X@5w8$}u2_r4+@MU4AJJi(fkw=O&Ujmps%aC9YBtp_cb z7HECCa-4=H7fN$Cd)PA%!vrEtvL}hvav%E6D4xViz>$i!rMX7LjU=2 zvjg9-Htve%uC^0sOd8CVOAWDU^;PcQkJIgxHjT(AY}MIqG8EoQ&D=Yzeif|tUXbQT zy#rlEw7p=8Vnz+mpp?+%;CK&yDvpM3>n;b`(@1yv(VlWPHbI2scmpS|2#?uBD()BT z&NcW?S4#WO*D(nP;=XTQ-})6FsqNxTdx@DdqVdNE-OGg$_uK`>7^=^21H=q#IbD*=zozGZlL!{O#e zWAEVi7vjp!3Fljz3gl?_k~(b2&>RwpSZLt|{A!yipa_J4giqgz$Dj?~Aiv$T=<*{& zB7WKPaML5w50zHG{?%012kCz|D^2KbEP6i5ywkdzBcSN-_K*_e+3#!lXHVh*P2sW- z@~Ic_GIY#0SJ%d3Ez0ZWKjuDoc~Ji9 zmmaT+_)bu)OB9WR*pBmial`78w5QtAzee~5%XnfU(hIxsRR{C9-t73Z&uJNIpI!Qy z-D|4Ux~tpMs@wayVr8VSD0A?ZQwEw7E)Lm8c)fmBEXMFayLcNFdKp{s=u{IW-`UBx z_Di%nN2?+b==wo306)bBRppi*rOtk>u*BU%mJ!>=}j6&AKE!Dwp43D2Ko z09nQK#^#Hk@H7GKEXA->aFrn=<5&f+d95vDhPpwKfpm#XR?ln~{!!ObNnKFxY5(Bi zB-zk5E!^lHi@Z!tlN9_rCB_+OCPg@1IjHTK`viP`wFn~S%aX%UgM)%zEHnD!wXk2 z#-{VeI=(?Be8+OUuQaZE;vU^e%y;;9v(&>=&BTT)HNM%^co8$?REH^&p;kIsJyG^- z_%tSQ3rik9p3S;?}4&LXmBlaE$VOscrYDiANJVPt>R6# zT-)mnTiq}pYy!?Gtjh9*2hddUbE%r%uw%^^&!pV(*Sz|~mm`K`TAbK)X?@CYd$2I) z*P?l5l7-~|)v^ius z7fA%CqgAU6)|gw0d0zDNeYwFjplaz~E^jB2<&ts}&6yY8HFo)IN4ePR$YbiD_`|WR ze`Jw$#9h`*KS8>cO_z`})-6lD?p{sI53VKW-7H@FY34&uzRZ68i(Ka?vauYKjA5#bk2WGNz0gWnOE-f)h%jm zWA7)^W!?+!3x~OGsB6eAGPnwn1vajAj^sO5lgnU2}8}Iw>(CAJ-ochsW}-V-DuncxUS74i}mC z8`@KK6*8GetHnBhd2@ZeFo6=y+El?EAA6LgG0|8L+u17|BMid>a;Xl?rrnF^UT=E& zNT^K>K7Bgoo)e~T2FnQM%(}~ziyUe-RCRxKeCx?jxNfR6@I8vpSZCpp{7JMo4B0px zpnmJug=;**z@u$a=dN@8=FO6lAi{`6T2{C)Dg3K3HweuO|9cmZ&&N0upq+nMxH&z- zdC+6n5*>LZfXKZAk3VJJ>jVBzzW?H{)*=OC8>8!6Eqg%7J$cj~helNj?`x}j$7;); z8xE=ldBggoBjO2Em%Qw}aEANAdNNz8@{fwur?CI>V5)reqv#wdv+qvv(LN5&Ku1)5 z!uC?vCVR}UwayrvZrQZ=Po{>uGyM*TQS`4qfqm7GOL6}V%eUtJS~~Bvne$(`SvU-! z@&&)VZG51HAjto0&qs}kM?7ERUe?XOd9f0=D_zw76%mTQ79&*T+KnBf{9+w^Y+}2& zzPTrAmn?b9w{PV)d!D%M7mb37_UCu{&-?1j3~P5eOMSS=alx+)mTUT`S*s)6l4Zar|OX2}bZB$(ld396rm^prA4py3HDDZ;&FXE*t&%tPXblW%ZB^I{V%un5@p-qYW>u&m+-)d&|B2$Z5S$oMJK6P_!sK#bwbww&$mt63#&nV*+ur86S*dG5tSTeggbA65oq4eDrr z*CqH}zIKJH6`9SHSbpNEhML#aRe7R6FXE6@p}bFz+4hyktH+kaAH|mWOiA_S^_$tw z2ctU!$**Hr%e{U+e9FTFhnv$}E3mS*+%PZ8su?zBpfzKilaeB_xPZ2>l)x@WwzbhZ z-Op#)@#EUnORr|-wE=to{`wQM^=%r1nJY?Y_uCr?6dyGAmhW+Urg^bRF(awHQaP~_ z?7ruw%T>*VZ?q7gStGDG0f9NAG2flaC%X4$q0?~sN13g-Gf&dc35R27`QN_)2Z-%B z);X_nJfg6)Dl*5P;?OcGa!_{C!7cW;IrHZD z((?RyE=Bdn$JYMG(x@7rbFvzJF?VduhWzYV3E%6Sp=@CxU1LwWNkm1=qT;Fjgdl!r zJJ%48GwjWqsn!^Us*Dc~G)+<0RVDxM!)! zyZ3{4LwnygQ>j`Wp}Ua@EXZd~7_=@(!gGt!6Sb%ppu~9Ksgl`rRgn_Q z9*Kd^SMzK;;QUvB0gcvX&mZ}W(oN@F9~T*is{dj#)Gm+e&ekos9K(-h`$5^j`&Vz? z=LOf>rQ-?FOG2a%-OlwY5^5{d;nf_HvQCIO{f0#^fE&cZau>3PreUVyP z(wS&<-{-H@ns65j&Ll|`ah+($Y=8d@8@xwp@5Ig@Uxk0~6VKsYy!A@Q@g5bLhxO+V zd8PrloAR8*;P`v;_upZ;kJLNoF#+>`|D-hJ)WaqfhK7jr_vPNjnkAocvK{zy*>$}| zDBR`^VXQ!mzYPX*p@>&Q*^?!mM+;-0jxS9V8(c)HU7sJaU&&v=aOM{QN8xCG>v!Nw z*-%&xC=n7|(C3LwYXk-#+1=?8hvrl0&yKujap1&~lkp4Pj-$P9Ow`Zxf`egMJJ&wkLW17<&x zw*Bm^EbZ|nCjSfJ8T^XyIQ@H(C`ssnPCooJvC`4IJ1^!d)>`txB9*ga?3(rwSdlJZ z6L*CmsJ0h1nng&ji}WLN;fa7=!`BFhUbk|<4aBjk8ZGQO5TXVs@}5j3M%wEygv>3P-@$n<*B>2eSbqBW-p(lAEAZMG}Bj`_46$fN7u^C@)n&u&T$~h-U47gK7|B zeV+~49c{RXp3-NbZsm2QD7@E?ws~Fsp!PHsOwp)@CF1cZIA!)qU`~>jvji#N$_#S6 zcubg`K&v*G{rZZ{pv-OK1ny}PV8SsEf=K(etUS>e#5`ni2PDKIe$t9pM3d32yJ#pi zd8yJvzpstqk+k8COWb zM@k|6M~s=One1n-ka2O5uiYxA28I-c@UAL3%2&gn0&3C2&&AUqG0+2Ef6k1#GuFj8 zw_W3{oST2Vb%cScHlZ&9jqYwOSt8{HjFI}{%SXZbXf?unq20!cpE=XiWU&KGLphg(E7>9;NCTz>7lL!dfmNP@XZ_UzqFkDCfr z7*vr`kr}$7H?TW#nPZ43e@YH=8{2@oBIhk@j_nWYx_%`3C+a8>SoX=IQdd}r2RzZh zQuO%9dh?J9N3L2HZ@Ok5bmJFz#&nN%RapWOaHG3JQ74}vwouuo>(lKHP$D#I{RoDL z#B1tbBW9$t>Ny}^=*|B$=d=tw_$Ulq%GVMnPs!TNGY0Nuj|FHHTP)WPKIZx-k?my-!e5|@kTD%7NWD=GjOQq?lTvv1k$HyD zm!4CLdiN7PQB~^_HX>1!QNR@EI=S!!9A--=z>@NF{F57P$P;EK9|tlU2Cpd}fn|sVVpi$?BlMnN>aEsOmcLrvHm|EXK*E%cif=2JPBg85 zECBIA|Ixw7k@6o*sh{uZ|8;Ib_SC<(#cm=11PN|B{GQ)=qxO9ouxacyk=zdUdfi$ z=xux&Q|zNFtZfSYhvDXgJrQY#U>b5mtt$wi_cmv%J3>N>ReKhSgs%{0IJc;6PE2t| zLJOF5>m&4Hc>kLkC(+#N?;1=LV`+2UW+V4~Q__vzG=b*q( zwEjsqY+CL|*RhW!#jcWJ<>>>?f29dE;3=n7b(r`I$){SBn*+a}N4rZPYSPH$o`F!g zwAW;30_2=?Xh$xJns@lL)g;*}Sw2%@CEdu@D!s{IRG;!8AjRp3R{F}y{}OSOb| zMD~gAu-jKEZ#f~R=lB}|@`GOPQNKjtO64N2edQj19pSbb8hKjpd2;B%jc z)lJ81yOeNUyqZaS^R*WVp0Lr`lNy?0>}>7u+H zKP&Xg;=cSiq9H^4$Fgr(P7#7o3-Qy9_=vp%)cS<#AMtp{Q<{`b|52;9@2Z4*YuAN6%LXyA|i(mXETD#eB_A# zzgPL6|4;uuoR67!Qjz)&Qe*;!q@A-B9$u4zZQjO~0Sp8b1rP^))C8`5Q?#E=y(WS0 z+&5wBWhhk9>uq^3V}3Y#> z2Wc8_F)-jA%wR1cl8O3(%$W!ceczB1t=zYt4PL@+2YE@TcHX7yg&cjua9HS_|5tz}DX?Eo|2?I8){2I?+(%rY=9s=8A}+;sfFSG7zJ^^TWSu985B zsjv1{r63sDt?HMTJZ&y-1CPougFd7s{Kx%QlAu4h%9v)Ww24$3q7-6JeZt9&q6%jw zN686O+D)bE7ewDO`rf1w`@#cY&;|eEoBDRke+v8Y7KiAUp+&~;v zg1xNFn$EXR2(xyn7bg77Tptvx{gfzywV>*2{Bsi7e|6=G5Fd~M*q}npY<-`0r+>Pg z_UW!~?DzlZWdH7ghAF>Iy?VPQ1YeV3;L!qkoVC%D{am<&`B{Lmb8^%WB?ZT1-Km`3#9B(eXKx$WcOd=Z?%zY$zdI^jKertM2@ zI3vNcB)%1UO$Db)M!Ki;bk)^CzcxbJYIkKAo68gGV|mUO3?!CflVF|c5Ykbe)dkHY zxYIUbF?0n%0D>BL6kVXR0V4LImi}C9Fa}A~s9>n!E2?j2|M{@#<&|Jpe?Dttn|JJw z{&#T{BZVH@NGI&SXnRu-h_QQOQvcuk=Kv;|EN1~KM=nsnRjAgepEL32*FZCadci4Y4-j~6FXq`r_*dlhY3(y?X?zC`3W>_GD286d^fOnIk zf%+66{5mL6Z{X3kBJA~~bFnv`6oLKQ&2^7$L{Uso_r3s}h6aF>5@mN}X0Dc_=LsEG zGXZuN^ltKddG<|dUFCK&9c@~t1|$e4E9t#dC7^|tsU$OOiJ37d9mYeFrvV7s=Y1Qv zYcJ%@+PBrlU#PnMkgp1jQ zKL%*Qz4Kyi7GPIkJE1fzT|WqKji@c7bnfQG@yn>@-%6$bj4)S>i9~&u{%pj11JD+As5rd# zsQ~D~-LqVUO}4QJ#1kQRBqg6w#WA_P9s=n?iGT9|7v1Xt3bRL6rOAVQE}QdsvWRr> z?;f3&Qin~s;jacgV!Gr%8uY#Y%LYBA zYS0Qe&1U@i3_0!{gEXVEclt&NrCrAROG^y{ah0vZQ}c>@T>_8Ydt75T^Wz^2b?!N} ztdz|a_v{Y2xroL2f&je_9$Cx`w=6OC1U{OzBMUXPAM-sTwhu}1p!5U(Xo`U#lY6HzaCD>M*abe zB%Zb=SjlRk7#xQ*gyEI=k#a&i_<6E*hmr^->!>XS@{B6x*{}{*?DyHQZg-*LGhR3S zI)Z{uTysG{LPQsf7jQo+>zxM4MExICO6e-iWey7_kP^3^uGJ=aGI4s2KvfS70lp8> zmbm-{KHT(#N+i*wtDB=I%16SWasZfF3r|~m5apgL;kufAw^g>^qmfV9YnU&NvWT(4 zg}90f2@Csi6FW`BX;oj*dlR%Qv0!TuJl2}MgolGR&fTa9L8SCw!^}A<<_~T%JSr5cx`Hdx{+ShYxkR z<*Jhj@5?!n%chf_xiBr<)tAW{`}F-GWM3{ok$O_MR`+Zp^@MoF%@et^CfXUP&&UEK zRU~^>$k)%`a4_4>vkPX9vAVTguG?0g_=*va7V|~&xBCwbbWM6PjPb;#UE6PvFFEb) z!gTuB@!JJJwg;A2{Xjz@3b;r#ltRN5K=gPkDLbg8I|1eCrqorENsQqpcqxz;xZ$bz zr_a#d6<6a$w z_Qk5khfO)H#HX^I;75(|tAcDgg9HOc-ylwtE2^g|TFB36WoQ<2*5Y6|Y1y^kA$B5w zgeLN{8b&UL0)*VF5JU^~%z!k!82v(bl|gUprul8K8+?$H77H0@#3SCi+?9$WRjv!r zx8*{oCw9r`Y|mOFk|>8*b|YG?((xWQAw*91kB5M|27vx0#n4Aca~Fw&`SHZ_tmJP` zcxFwbD0zC)d#g$2nDWCoG<&cJxVuBR8XhGmV7cjwJ|kiLLA~!!Z4;19vSkZuRpP;S zp&SxsXI!71l``TMvr9k`%f>Lzz|jAW8bLa?rBOn|}AT>GpMgq0r5(J6oM#SMX$h^!q6upLC<@X=>sc=R^Mf9VPk%pqp-jt*qY@ z#if1Xi{gEV&wtO%K3B!^Eyr?Q`Pz!C9e5e?;)GmwyB8IAXu^ItS_Wr(vo85?QDP-h zldd$-x$i%Kt~;nN<4X@%$D256gFVEMojk~Y2(mkGQDv%nS2@qH=Vm!K@4XX$65rOB-8ir4gnLhLCdo?2%Te(R z>(~mwrxRyCh_#uRsopMh>)I3MH@|m9kLYGGWx4C32Oc?_51;WaJ=Vo8oTaT;nBlMz zdO!EV$#w4VCtU9R3>D83&ulxXwd`9{vY_BXLFWYwTtR?IUEy{J+xA{F>${?8#h|9| zvcFP?t$OD&_NhxQw)@ksh>++Wlgt@u13RBgt4w{8%f>AE zk7n>#o(?*Lt&eqlwZ5-HqkP6>_7v1p~=-FBR6_kg_em|V-I~pcaLx6W? z`Xe~>_aum(>O9z?{|*|5p)i>!i@OBF>8jahmoy;dxR;1lhxqXWfTJy52-|C`2WqsD z$s#@pfhOT>T=#SF7bJC0A27pa@c#f5Udy5N?^Ho#Gk@ z@KpQM(+hPfgzkZ->(el^?d5{9S;GNSq7V)w;bUIay5TTy1beWGV|B(kJ1s2QYxpnq z?avgUnVG92c7{>`3$Ri@>EIbXzlcTJ!vE_`#C3+AG460V*fnGb*3#(GjZCO?$?o!l zICsBy`Qs`jH$n5!(npGp)72Tj0PHGN0pNbQY&j?q%e*&C#N_*CZ^A2FsTOKWADHbEUp=t4*BP?4!wvG0*LGiM-|!kCPbq;uM9b|YPevUZ#E$179>Ohf z7z=2IC;ogpiz{lD)iKt^W*-axTpy?gKi3IE4B0Q?J*y9GUW=-d;qib&cG`(Yh9m9; zjE2L}Pbnj5;?ejLc~Gzr&Duv^*A_cC|EF>^fT&GLjEc{q09v2YA16&-j&uqvhNIN$ zIsz~mv0GZK?Pz8NvX9Of>28=1@+L@V*-gJ_8T&|i|Ch5}dm+n|w=ns-{6*I*+r-); zAFJ4dy{$kln;dVdgoYe^4<*nMH}1xv z631zt|4IEE4+DDQ(?_ICGC8jI$fKe&SY1%=ObFpn!MV1tyg@O&B<>jgcPQ=Hw^TcJl;5)`WWlh(b~kq(nwKc zb#u<#?34V44{UqgPOG*%-zItz%rm4|*$md8oxwv{vPBWOR)cJ(_Z$s-8BVyP5S~o9B>i zu*ClTjS39HP`4N(%LB^{@EM$%kGs~}ts7s}GQ78?nSCXnIjQYE&HKuRpP##I5*)W#!gI4Ak(^zr ziRO{(u5d!W#=P#@X6z(N}5t?NrVctVRJ*}#e-R?K}D7$7Qjj=k_u<*KZ*V2=sGge z@07Fa&wb>700QVuxVeK|)%y>rc+CQ`UN0==W=TxdTkJgSr`Y^8aL3}(=M;R2K~H1s zZ$e_tHarV8igY%E{!Qo21|7^^XsS{b`6r9*8XRyX8;61>nL3VyH9yv2e&B&=QAX}p zQ+GR&biVACjIzNZr#-_b`*gkgdLo$z^|5^Uu|GU*cxV(@eGwS$ zt?s#8tqrBW3+7L}`GLYl1i3BJg8F(c@YdqJR{iKuxi7B_abO!b^ zldhBpi1TJ82vIj73Aqo#NA$qmuK-`ED^s%LwpZ^f`N4|qz%<;2WS0)*o$Cj1i31t;nXm+ zikMXb5^>Mbb=`}y4ZDSQT18iTi!xm;@1b4znGv~khbW=FiR(qygW`L(gYoB!Onq&b z!E&T{Na^m(jSFi$1!>lAhT2O$(rC?(2Dk#he!L+tA1kff)^&*WUv0vnEK=9;}D^%zg7Zvqgqm*tVt|feT)E5H}R!-hvPnE079wc~;Vc>ytXUvVF)$?b7Stb?6@&NRB_I3DlEGDA8 ziF!Wb+(uE!qZ`zKL~w2+O6TGT4Elz>5al?`jV9e3lP4#BzNCCC7JGHG_x>H}e(VUh zY3GBz7>t@IlMHwLfs4FPhvkbkJiQ+^(wGYNO6Tj?)xTsuT2Bl#=W993VQ=KvOS~rD zSKE2GP!$jH@G994T{Ux!eF^Yn6S;pCw=Q_`wHD*jFsW-K4((hf!z_L9nSgg8LT*Ty z#zfc9{%q=__@mXxz=M%Lo@9(1?GKglO)nOuAJT*#kvD8CSM3c?PUB~cJqR>wkGjK2 zWWAOY{UhZ}@hkV^8@wkqIOc@ZexgR4P`_t+P>xb?>6j^}Ddj#V&tJ5syIDo2J}Pp+ zJ8@C1)aC>Jh3zlRl!2pE|BJo142yEz`-T-o5flVLlvYGaz@QsRLAo2IyK86=1QkIf zBnKF!yJ3I5#6W2N>cxFZbH(-h1nPzwg)QIqnbZI94EQ;=0c3|NkXArfm2k zu>ojt6#&Qmals(SEtNM43d6}ssZa8%2x~Y76|5=?~ z;KXU=LGerZ)3ry)^|}?Cr^p8yMWSLnlD)a5hK{=i!dNn_^qk)d%#w>Yqcu-W{&B^o zX5wMHnzeF%@F@45pRxW`F~p?_i$|ZWs9av?PA-RPxHUc?*J-KbC?!WNnP<}Y<|I3` zUfP?EB7%OD%s>F`nyX*$_Jyy&bHOUH(#p<*n&4kg-+$l;q6+w5;DQ2-U2Vm`n64Ro zi&_6yBn;-UKk3%bKDX&+x)=k1q{IvXl^HXj;0Qg7OQe@Z~G6u_W;5dkhnC`(XUY`5pDjQkra;&HU9w5-e2Ceqefqtgu@OvFTG^+mCiIxXp9gwIKqB%_vaUGoz6W)#utGHS1nZ$}k5<~W$D z67*rg(o;f?yy!!jM|$FI%f_68^_m1VTar@W;ThAl&k1YK?Ti_Id$`j@+MLbcmaSlF zfi(Y7cQ$}Jr{)MJ?5%8f*16qv&H4Zj$*{PeG2p!ASg#MpCo6fcaOgDx_h|V+zQSWS zZq&APwe4w=(v!&)I{~+~qUXV{zvfNOe<1i=efhb%g^LA!b9**J54RKsq(6%rruMW4yRMhcf z)hV}8J$nON9ODrpdt{6*#gwOxi1zHMp{4EOrHBitW<3p>3o#{M2`kRwf1%~o%2xaI z0b5G)&n4o&=sB4R@J38tJ3+~hMjW{PSQKE9*r&IE94HUS>$GVc+hf`GfS#m=3J>ZH z8#|PBqbg+j=4@sPBAGL+31U4E4E^oG!;-4_F-&^ByJPT!(We6F#qAQouk*HvYon73 zRo)j&U(-;Pp5%1o8W&(#2e5`9j+Q0u6AGQHV=>!!PM)R=sZc$tVW(s1eEPw<&<~r? zp>q&k$8YYxcKH}Rh}vSXI31LR+gB!gBW=fvxm<*hLAW}F#Q086SwAjCxxFCaI?WhJ z!|JU`iS9Q9>fYQp9}Y1Mu)p^QsF#oNDk|5wGx2+9~6@Bq@e4o1UH z6LKsFKGmNRLlEw{m^1=4(a#iCA)#>)%=QqP@|i8t(jlGPYftSqWaMf(b8yWdYLI#_ zY$>Ap!a-i=gWZK}_6?<4B}aifop>S21X56~zcM9{7sX_5P^i4(NE`t0`$(1F_&#E`sbn04Vud`Xs$wgNNi6gEYMby(q zyG7PLu&=U&g9CdugExU3|0&br)m)3{FM;X=&uyPm|KS#N0XyRp#yNioJD4DV>Z9yn z2r%N++-YHF6isS^>WQJ)#Oi!aJx57EGdQ|cg6U5i;!1i=aqm2j0Yc<>-)Q4Bhw0A{ zX{t*2l8jZuB&)!h8c?SpyWg~WYIU4>>Ut7(xb`;sU+P61d$i~mI|d7FEFIfzm#3Y1 z0Xw=VH*hXDSn8%)`4!!vQAQCzk}*<)i89;wyVyfdOr6HT$hz}E#@ycin--fdV?3XC z0}14FIE(SW5?#VQ+iQz0Skstm$XQ1^-T997F(|U~@a%lWH|TB#?^b(K_(SX~A^w~Q z@1XurnPEoo%6W2)h+pWm!j^`c|%wId_!@;u7cp z*k~LB6hH|Xg7~ozoqHFAQfn*;&%{s*xG_>|L@i3lg?(mN73qK>m)qnWnqE+@V&qZg zKaNLZCP#1)BUQ~FyBqfItCBaDN%>PF_1&NIR2q1a)@z^0OC+<6R{pT19h;ZpR1+u) zIY#t~jO~fb(JTv39#2zaO)Q(we2Io<{AdFeky2vOEGlid_xY)Y8rPh6k($Hylx~Xp zvx*tsL_#7pO+Izy(`)RFTXQY$E|cga@0Z6#X737@g}K_M-*&@rMSp(;`8U*G4NY2w z8794R{srFOxdZMTAd%7q3S3)|3b(!hDfoX;SrdLBYjh9L!!^dtVNQUE{)`~@rmquG z-f>4w_;FkIev!}QG-H0MO;z0!(%=Q;}9PA5vF9!o|4q4G__)4XBf+XG5bH z+gH^-oV+ z_m;Y7M=iw@wfk5)@(jMhDts2)RK@GJZfB7a;nIYZdp2>+Tl3S+vEjv*z7=dYJK`U2 zKCyq}3d1^CJMZAS$;SANlqeaWJ~9-xT}o5Nl_a=cZr%anp))u+ufc{iWlZZxLs{j%SV;lgX-u{>U({11rwj zq$;gLW5|>gE)9*)njX@l5@J|%+U9tfjBsbxGv5)k^RSoN3RDDJkrl~)V$uE9&DFxXG}>o7+&rmwd`^5w zwZMZfZimmnXYda|@i%CXuu*>d@-N)+w_y?jS?GqHgkc}X{s*}7|KG2PjEtbUTloh) z>0kVwXbHsX=|=zjgJkos7Zw=-KKoh3%WwIA{h2?!ZZJdwZ`$=akKf)< z0wOTqs4-A`9iEc1?n_Px6r?usfM%kta9&WzRZ4ptyU=P+w!>o%ls*;KL+q@e*E)t- z^)71sHs%l2L?$eeo~$+*$P(g!V|vJ9Gy=4zzqCd`O>7P$23Tp9d@}z4;9>2-P|{gu z_mCUpE%pP%Po<=o>!z=l*JZ9KB4AhnBIfu|@$5}Z#i(5==#FUV_fHvm9e5Gs)IshO z2#on$*#{t(6+mgmu<3Hwc5*yT5gdlPw6S1KI%WZN^11p>-xxGXOulXKvweX@0DjOB z5{cEF9Ijl#y?2}NgET775H$gGQ072|5?U1m7;tSsn&g>vBoMD0{NY-k)r2m+-EZ?h zu_|5GF%nWYsDS*ZK>DzL3UDB$;|@(%H0r%QS0+(@ZJZVB1fvq2X>^Z4a4@u_-^{UA zYXiPA3n+;S$dp+XMJT!K48b8>0mLYM*!(kLQU*Y7@B?=U@J8kUIy93g^yCVGR36!w|AG69oIJnfJ2ZB*Cs73#WKB(32QoQl1To1ar`ci3j!)#mX5T-25PLBN zlr=seC9hH4(@I-)MG(kw*nhkiDX3gn2 zN_L+p1~^nND4!qRfUz-6rBd*zSGl5R7XVtydA>wU?0u!!y1%=8{S`?_6?{aBqci)Wx+ zr)0>$s{>d%);D0d&wv=|+6hfvsyQ73$l``UiWNOK>r3fc+s0S0>-?R-$Fu(S8X7I< zKpXqJVizLf%ay_TWsq`J>CCg%$Xj++aZ|XtjY>ilpbxcZ!;0%n&Zfqd;>MYUC*X>$ z$#$>G5Kva;m{AC!hzp3nCY* z(8X?pYO**KYe#w z(gswr6ew?iJJ8{*KUBamFjU5IsBF9h;>0b$$yoyRdJlm3DLHMSFS%;jGZ_gz3bHu) zA=0~tomPIkA2o?!vQFoBl1OJSU67V#(IVq;cyCFyZ@3|Je| z8^j%U0&_QpM1&(hozXHw7w6>m=!rbk`0R{py@O0O`64@(=dTTDq!WIpll^YA8MyhH zco35-0KcwY`c&ow#Q{r%NZ~V)zkG#5RQxJ1y7<+eYQbww1JADtywm611##KuZsy(t z9ZsH*03RBqn{z(%|3;UI(q)4?D5~vS)CGoAk`9xF|X9MtNyx6 zr@>*mdo&2cC3f-AUMu9oa?~u|_Ve^!5Adge0nlKDqMeKE>3xR{fPBBq#Pp=asA+ZG z<%l(Y%h#-5K_ZJ4)!gxc(&h7YK;EH;iF{rUo-Ycl05y`h6f<=#^##IY_f-im;P$I= z&iveHBC3R(VGO06L{Kxjx3bdxePPSGLCH9+0E%#j-pSOdGG$*Bc;X@Ho;oDrJftX2 z1+MAPRFry7;nEQ2NaKQCx4gj*M?j{1nw$_5^4knBkcou!`dvefoMThnt{vZ=3 zSQcFQWBryCPtqF#PfjFGZSmLJ|4>XAc^R6^lxA;WC8xliQ39N9!r~;Veh;kl`f7iI zg?T$yuPHh35-?2Ht;EwzT-D+QZeM+(JwxAvuRtwxXNXd}j}D5|<2uZ4DW||6Xvpfh zQoo-XP>(g$4}v{nEJ2A7!o4_Bo7K4n_g02DW{ge`hfoindXXyMiu3DfeGSt32gl>) zTcM&|y`~a&U3w1A&&}j@LBbF0Z>#bhG+Uppg9KGmpCL6Pig#D_fzAGBAaR0|H2ve} zyPXjej68lAF*1Eb#^d(9{V9ba1uz!@mq~xWzl`93bNj*gJ?RePdCs~}n2NbZzYO{1J5b54de=SX zJd%DZgB?gSnNq(Tkk4$$8T`&yEm}{#k8+8T*RN*u#=dmGi@ISE2d3-SNV}tjDQssl z{Wg)7=jHO034FZ_4%CcGVBaGEfk?W8e?7du8o&+uXXFvj-}27jRr2w+(PM^uxq;qbNS5*5c#asQiJxNP^amzRy%5E$1IvQ-!;$ zOwxVOHqM0aHPaeyqfkJhTF`<4h8sw}R0IJs(}J4dJu+RkThJXRjzr%> zF5>`GtSh~AAZ7nlPP6N7Mi-IfH(xFa>Fs$|R5O^^rYX=dA2FaZ<-UA5LIrS_Jp7{V z!13+!#JikIAKdE;8RKDb)^}=rS-dr)tPFu@cLj&7(I{Z>Af4{bDRO4W)NDyQY`><_ z`M~wCzx5g}3**xdQyEGGc6}v*39`$1Cg`y90qK|pfj_`$)Zn5Y?n$>xA>X)SC##X$ zgJ1k=-g9Vd3o<*)Rn%Gyuh9JPyj2A;M4#5Z1<>ss$ImN?rO0|p*%%D;+cE1M> z-~C2v#j}sv>;jBB&&mg95Yq`qrV>+40uuQH3uBbK^_QTO(v)W)T=$o|iawXLK}o4s z#8^L3O8Wq1J^lLmA0)~$C887Gc@s>|6UOo5`08BeYW|vtJp$q0&U98usu5_Yt1W$6 zi86*(Y-P049vyfkfm~Q)c=Xtq`n~H--2QeXsE#70>__NcDjuNAZPqwNG$g?)gaY4jNg%Ej%tuZb+Pqv-Tyls4kJ}cfzrlpWCB1R^uB9Ic zme5l%wjG#!X`~PYzEnmbPYm?rkxF7p+k=#V8C)t+bHJ-nWr+;zV&=6~aS-|w*7r|k z=^X4V15+J{7#Ir;g7ov85#B`>R!L$(7pkc)fd&$h&(7ajEeq$Fh^{kaUfFv($c;)5KHZJ1f9ea&jso^d^W8ilX=WhlIRZ2=b`B{&gH@}i2kI2Jlg)sg7oV;!5 zG~!1I2CQ17S{@)6U%sv%sJs@XS}hXT!&7^Z*nU+-Q#@g&h7GV$A;*Xtv*GEJKc_rZ zo>c?v-R{n7lxDcF3cW8tVDx*9Q3inL2*=+X#q6lk!@YY-HI!_?5NUTOFvYmYB8T-4 z8+>FR&QsfHa4Y(u@V`(nr-qntr4oV#qJw%eP5fft&K)ZyYS5dAgeLLUaDJsiyKuo9 zBEGfOTD1VZ({nPFzqk$V(i$mU21)fM1nhOLdl7{PII&H6*up4^u8sSl9?xyQw;w5E zj+hjC4lK7Z6GITy0mf%L*;?5M5`(&!oo0GfyXJP^$2%o+{ zxtzLx_KU#WJa%V#L~pBlrUcN;klDmtCft7!Tp}S&))LWDNlO3I0kwqVd$I-E)yzF@ zaPiVtvU~hM^Hc`S3fN{#Z$2`65uuu^!4~%c1MXIrm3{}v?q6g1Tnz=pGSf9W-53yx zTIITYGw*_SM;}<}{DwqxrN$&M5xn;eOBhmnZiU4HrV;6{n7-?*D^!;#TjT{O?y813 zBHtzcf&KjRwT%YL6fqlMazReTPIz@xqT4}M$0`m<46Ei|?pS^Cgt2<-y@;0?S5ys% z;dz4I4T3ePsB6H?>)^*o>Tv1$aq=Rt@(ko@KdR!dr8?$eT)ay!@|?N0$&er?)Amrg z8xm>n0Z|W9RJo{NVj7A@I9>~L&olD*(^gSO29$QkRo3=IpHJ{+)z5N?f;iy`dB=|? zIUw;i%!{=x8?-qrJLnH!#KNZ(mVNR}vaOV^a4+7i@!Qb4M;{sQza?V9!WK@~jm*6; z|NYg3`c9TuKmP816x%k*e~%jZi|T=s+R^gIKf#?pAw%a$ zJTCs{@BRBVfT)WC(X=YvAO50*mzXTGqh98y<)|^IeN2;O$vxjJ%LxP6;6iH*^Sk8| z)ArTWAi@$723ZJ)7~=!>>_{p;Lto*n6g1eZK0oonxH^p-*--k}%bw3usd9K0=fVf` z+QF<2Vz4Xj4&Z**09y%)#Z=f6t~#&mjQH*4UX=#m6x9ulg`|cu%f3~xaXB*H0C^{1 zm1OIC&%DuHVk)PZ2?au<{7zp`Jbff&@g^LoGfSaj2GD!=3>19DA7fCb<&aApD6CTG z7hXGF^7M^KQ3oi!&6iMOVhqTZXIuH}`g$&8xc5}F(ZATZWyJsNIlYE<+D}j`BsfP{ zbu01==Y&gj{XoY#y9N1W3PB)1V8&n@uvD8Ao4-qQRJ8oF!&UaD(S^F2U6zIrGeZ8; z*B6Y@)3#vRl(jf7?(x#*9_c-;?g4hNJ9`v@nLi{>yH!s#ya2E;->yn@8$edG#(3b# zH}$<*Va7}cJ~V+uiam9xJ^xeGuK8=f(TAc7-{=(imB z3U~MeThZ_1L_Fc!TYW!Aew+8{hM~=ft!=Y}VFSdY48#3SZo1RTe*%i@qK&=hUta~s zIm5&dBn5SQI#Tp^@Ps@zjDWApvC0i(L_c~XS_OiKzANTmD`;k(01qh> zAP6sq0*u)m96-pzC?GV}YE}xI=}AeIl)l;dtT{KFz4*bmi<2S^fOR@CQNb8K23i3g z5I^=G7lI=IOuqzxwTpNkfLgYo!NgH9gINP;x^k;Vk1upQt9P1{#e((fnpM``u4cZD zDOeRw?i0g7lu#0vMK|5^vog&>anv@w zAD=*mjbTs$vZk4sf&rZH=Sc*4U>8LF5+~MGr)cj~7)kD?4>|6zXW6B9F?zfit2qKi z3h^1|`bZ5aj;E!27GVR6eqYHnwE&lB17iO>>5#j}KJE8ec_z$%`s{IB{@n@^iNu6P`_nJ=3j!CBJ*4cAHY=QxhyA%KW_^5I zjXxM)vVO+ONaJnr6w}4EtY$l|&I_;S4>g-m71?nWuQKM93t?QL7v%tfy9SVhiYYZ2 zsHD|;0}5TwhbEY_#0)Nj5N7ef!>;l77q zn0U-^9nsP6Kx;Pr)#0`TeoRO5JkD9JyHIG3@WJf0A?I4XYA0gNPg18)tOb;7PT?Q3 z!>iR#VDQa!APXw^L`o^jCvF24GoB)q>$n~GHm0XA5=LGdJxh`DZAaN6L7{*3&RWM0Epsn_9|Bc zOXChyOuVl7Efm}m%dj}}7@HBeNTK(#C*)P|uEuBLwCZP~lKBDR&nVyB;tuKDKe+u>7PJ>? z1O^0GA%7h6^cINqS_2tYVO7mgBQae7#rpw=B`;&FY{EqYu@~ULt!CEuHZ}*-tX@z( z$i@K0Nc!{@d&>5%xa6Gh+Nf`b$`>+|oL+qe5mcGs%HriX7g#_IuuI+ScwDy#C{<3d z{&)hQgQwxMVCCOS9LuCIbIq3}&F=*sVgZnZzXsX@9q6gH#OcH^Yp*Hb2xn~zeV0pz zhqehMPgJ#W&++;uO8GD?l8$Bgzmd9^)gyRAf){uOpYHP5{4kGb_BW%OX@UF{0Kpg)hlW(65!mC$+#FH~XqJNwtcw7xm<&9k|&E_5{5BE$h~SI>tb zRS*R*bgncC_7rRIv&LihEqE?Q=!+M@V;O$@)V$Jyf7gOEh+=DC|K<@hc!-X28#iC( zse4VK~L8b0hMW_(LjHRu;9asX*!; z%JyhxW2oiIvMt%j|4xm8!2!)vEnu2_$Xf3xpp^+mW4C@~>1y?oO`)ao$BkzuuNm?& zf$Ea|sRH6F^SR%i!F1trm#heg>R9)4B&h z^~=Uf7Co`FIylrANPji3eWV$CFSzwHmTIYy=eG&16%K76B5>=J8dip%17(~EGp#&# zt??;i2-vF{=BV)Yq6T3MVj7-6CNLg>CHYA!ANFx3=xQ;=@sXX?kFGpYW|c3TK0VN~ z=NqWHY9Yc^B1O*SFRi$JEe|2Jw%9x}wO3mL)jYr8P(L8xOuwTQh&aQUL;oRKXP2|& zj@Hq3Z;b^)tM3joN03wkM0!s*E%HZejgp6}C>aC;mHvUc=a#CfGxPn7o|~XZ*zSwa zq7MqfP;Og^B!Gt$J>iYrV2mdZ9@@G8b))9*?-2u05XbbL$2?ig=KZ0C6Eo0sWxbP!OZFdvx^|uzwfkDBuJypdwe;J-b zd#}+McPKa07XZVy;OY86I+Tg%WfZ@)Nnn)0!azFa5TJ4o=K?-(+a?@?b(8(Zr`gFv z{cbe@7VzkIhY#nY-DEfY0?{|n&aY6H$Ao{ou6YAcx>{$&j6^PFrELRkeI-bG9J|i0 zw}^8oAw(v;zOi|6YEHdCZ`fyTth)5T`&{FijsemwKXF|~Xyh0#Q8&ku3@Q%v#;3e4+@(+6ZiJQ#hjk&4gg19_WQTorPBd^`RZ>+@QgQp^?umhETGaTVT_)-2 zOAOyv!!ve}o%dg6d9YqQ#ES&2BbQkBUi!~BYTo2`v8duX0N^bRM#Veg*+9fVXkqgT z07t8(Ogn}R3-tKn8r)ZZ6koR{^xKVuKQxsc+qOLMB-@2W5Z<=Hj|W9%U&ndKxc5H} zpBTg>Hbk6wfZ!ld%<1X-Uhe7I_R|-d>vMq_i7G_LkGcf=*z0yw`pTqYSkxbeQ&sN{ z>e9bW8E$4^@KI9?9R27I?auz0oUZoK8@WT&W97&d!qD6(A&Px7C!#R5D1L) zub~LcnV0Wn<1+Zx6FDtsD7FVu3mYEq)E07wSsoXT%V!5zp^x72+cer1G$U+#YTi1r5s2_cU4&j7m+F zP~1_Kly~?IPk^(2UfE4D-c2Xc{LzF9*A+*u++>}4`;W&`KJbK_^HVr zGhypAfDGyj9aS=#21~9Bf*pz|$k=JK^YIg9I5UJyT75@Z5Da;pHcnEv}W zwpV^em2%D6v6Ea?^h&JN;wd;x$K7_>i4-`}W9l7M5f}Rr`l__qn}$~gz^vvNfNSgZ zGn_|n#*86F2t0?I7LbeSuU7#!Q2}8-&>#*DM%O+j=Fn7=t{%ySZOJEQ>Sbu~D3QOE z&SN`b0&oBVMFxbyM%(6nqXz${L3Ae{8nK%zTU(hFg}XUZ);e*jnC znmP}E=3@sxTh$VCA;nWFE}V4WD{8c|tMs4~nsR)-m3xOl zR?&yROZ8cCo2g2?PXjnTG2R13H1}W%P^cM5O1XP<0>J4{ zrVMvAKQ97Cu|@tIw@GW{#vJPh#2MWUcUI(Xxji)MqZLn2j^*8iiJoj!zfe3J!a;9O z-mdNP(=GZvE95{tRWyqSU2x>cIwr|NkTtDEe}0EQ4&;9HZ@{&+L4NC!v??m=tl&F? zE&dePb?{&unbTGhm8dKCF}hQ_{b_`IE-kXKw!a~1U|XglB9PBFfmx(HCh*EZKGVCB zbyYF$o4Gxp&oTKB=GNw%CaQV={=4DL?G^5cg`-20YL5TBsISUEam48={eKEV|Ht)D zhl?vykrYUjtoz>wK+!3Kp(9LI`S-i#pEpqeDJV6G{Qvqb0guBpKGNV+ONu`6j(Mv2 zN#&DltPF`hL%jNo7Q=+l{#1~2d;aX`O853~(X${q-~96pSpH!r1AlcK!x(_1Ax*w% z>pQ!xn7h`l+6|$*!BU?8I2eC;oU3bS!KZUOB9ijoZ)))J0_wmkD~j`9tw-Ym+pZbl zg(3ttTi?+GSk)@t-~|p5fR9{Umk0B$kFglVtZDAPpppj}{(0EHgc&~!{uQY3$?;LX zZv>8NZa{MBmYT`$?+Lgrn?U|MfXsvpS>6JVbT>lp13;{;f{nHu;56q4*A1R3Dpmq^ z70Lu^8jWSwUk26!tWQ(r$1cvIndg5Phy8E92F!fkzBSx7Th4=GIw zYPV!hx#kzZAEY)g1lFn-QuklD8{K+#maE2XsrT!34uhm;aGaAPrib+GIE4#XH%PQY00V(8!U z0?`|ogdvTd7Dx}K546K~Vp@!B#Y^32+}glFJJa}j=s~F~{|Z zhY>^$1e1plIsU>u6geqwsRv$|P9(awA$;+^)^G9m0ny~(Qr;>Qow)Ef>|y^{wah{% zF7MU6s3CC0K<>0lTrP#Wm7N2IAksDaqWf)0)!{`@p&E`n$j-vJ6}5Q%2ed(5uP#)` z=RE%cCC#zFPz1P}QQ%GbVrEZcty?tnmRdlM@QRS!fc(zE`G|L`bAV&%;JgwiWo*$e z1;mj}nM%gTe~Cn(z>6yDF1zE6#^?8#ctU-pp_)Ke;JLvv>y%~T`UMUl#9MzMAEjzO zaN}CDyx&5h0=(IQOZedE?83q*5-5^v7rWC6+}%Nz&GWsKl!vCT=ppc9d~Cgk1`>LK zZp#sQ)@x&fKuyC#rkM8l9qKlC=0Duc(LDFOsLXDB1Pc6S)h;pd*aZW~Pl_hs{`3Y$ zKX(aHan(_aaj;h2D1gV2gP5qB4;E0c@!T|>PXJEj_TG0GqBNZ33XVvL^9DD_C-ptR zTf}Fi!oYVn=6>1*ka}wfPugATW4x`D)p5O~EdH~PYhe@HM?03X4VQjl@ z_yJ2>(A8rF%Q~sEWuO%lSD#d3qtBf|=M?OogU_B{XLvFg`U3HmpJm<+rP6nOsoIi) zg>0=4d=GIS^-$(4FnhnlCB}&bQa|{3dxOY-Ud?ooDmd@*KQgX6Y&=JlfmYLO3%fM6lnJWqqk|Xv`aH|gk)zORE zpTH602Cn8&fVDlNn2w~yxr9%+pWeK%s7DH771A^NdfdQXn4QIGtKia;MT8q3I7%AX z6N<}Ke=!ITurevxCjeLd|GYnq#xNIWlH@X8@|k4zXOTnSHC?#={Py4_CrY25~POBhX9 zAFN-ckng1>ep>sVC;12#S{>kxdnEFtm;Uw-Q&Mi9LOy8nu6ac905|^>*%@(_P z8VCzuwWiOIR3SP9qgPc9fMwn?KP%lEh-P0d)&~zUzZ;nB3tC({EvyL|-!2k+Cxcj6 zq~xUTSYcN#yuMgx2<cB_b~evWD@L?`LK9fBs@@Axyi?EMVhwDZwTEOXXv|B z&*J|Ow1{GSfE>zB=YBZN&4(s35|6j0(C;RW7r~y-9Q6wA{PE1>KPQX72X#6!07rlP ztlF80$&z}udomPyW_N6_uG1AsVfl;V3^5TAdRo*{M3F-Fw43PyE>6D(#Xpdbj0>6r z>eL;RlOXby2VU2icEM8K_Ty}$)thj2PMo=#wKv0O8~MQ~f2C`hgL0DI(3SM^hR|V5(BmgTK@2}O)`x(6sa^irPOltCwPhh^ zjYJqc=Bkd^Yae~ifj;1NUx~71)ocR8!RI(gii9LS>l?fd_=4m59ytFXu{6~)kiy3; zze0MX-`yGt7-hKSLwbp;+{mq8veK9W=V#Rzi{MqkMw1`+_KTis?-FGTz-!8g+|C za`zXn*^*i(+}X|3Jb4AaRX%SWeQ7WzH%)TMig}WOuI=jvl^o=p!I4rl7HVHX{2!&G zRh>CNJqqRM)}+sxRQi57d(IH7sl5%At&($O$_0esBT5eR{9Q68w$5j%0Rku{VgEh{ z+!B6&xFxutE1}uBbI3iL*}5NTSl7V;OjG2%O2+vVd;f$)?k@Z*BywiR;{0yaP&7MW zO$u@|s*ec!G%$lt1>soqK)~ZG;Bnmyx#nJXX z$}*{}A7x&K1I}s~uns>n=2WIwHNt4LujMM_>C8s(~~6D_h3%vFP?< zE-+$qj!}ZGT`|IIt-hsr8C2|LZ6X2hSD>upd}2-G^%wrZk^tc!LYgc#8&3+4>sWXK z;GK&=Fj5NU+XH#_2fJr4-oYqF0Qf`k6oNlEV#19C*gqaIX`$(m-X(JFGZ1-^YN+vqw-h}xax|h zbLH=2K)I-s?skbVE!Wjn;kWl%rnIXn3XwUgg&PeINe1LX+E@KiFsD&C!f>xg<7v~M`-On?0tAFw z2|Wo2lEsuJAT}T8BYoOaW(ds{l5kBj(%Bvv%Hw(vz0|aZ@FVr_Mkdj2xxcVyp(Lp7 z8g58u`E92E_@ZHQ``yPcYfkSE)J3nP$QofGfr+jfdS`0qj?NOL^ZR8lFJQVhc8jjPqR}m#hTZ0x~};b9l^I+_-Nf znjv=CJ+)h;apfOqNaI|g4jqqhQS)wtc0bWv96FjS+%inD6@ij*csB&_1fn5 z;e~=v(DFe#;If3%8wH;m6Z-aGhXIQWV1%S!s#1wIiU)`(F_0aX4A~CHygs!mqzyn zN4L?PAGzyl2+Jg$!jA>Xmo?kMz|6@mlOY<F>4RDq;XNAzvv_t5RMk4)}h)|B>F6qC;0E^_*w|sxLW( zDPKWc7x$dJlBhU@lhA7%tLZb+$`{geaNTFUTFUYI7fcfFuQao8OePxYIw|`xZ$;HX zjvL>_L<8L76U9ef1ykx83j?L9DFg#oF7VD!3y2f~z@SSp&g(a1BtQZ@UGAhyQ2z#| z{EaQBsQH_;GapV>{}1&{B*b8g3?*^>ZI1a{d?Ev(D*yXie*2jJ_jCE}Bl&gZ{qN`U zzn{y0ohJV;o-a(%G7JO5C2rMhhDe5<GuT;=^ky7(`Avgq_Y{wjWq zgn~%A!dYt(#&-$BSX*x{2F~T3?U4yHifex#R?TG9(;1VWw$5ZeM$ptq)%P zyL{nqfOM8E;7cYqKlequRo=;0diUlVj|V?cYRAh5@HQ7~n)|H2($);q{rNs%a4jlB zBU?*|al=X z3D`SYIH3Cw>E2zf^5E@D@mPkgy8GJt8;;bc)P+fs z)4puAvCzVZ+g69-ZpC-e#S4F{Eg192JK8^TMm!q&cQ$5LA>hTiOAA-VOpzCwtHYE^ zhUFfAGka~vM z#d>jMhV{gFz}UuE{p9Ko5`3l(ouY==dy~hMaE**~po{IY(h#`s5~OSMEox39<9WH# zIn$G>Nt;PYro{@A1NKVYuU20l65(el`>b&%asgkHd-gD6T3t$_;r-~OdWQ-{O0Snn z8%75jx)1#_ch_vVEwZ(n&LUU0Suvd1va6*g)oyY%Y}&QsvEHRkcC}^xK9nQR8fqWA z%@tod&17v?y@U81TTYp8&r>>=7Q|JZ8CuTAjl8t_-jOW8c(xn+u6~0{AZvag?HX6P zXQD`nF=o--HJ8n>gnoyTR^Kf+@L6!)^Y|NX0h8fV24t)uzKVOZ6i%&-9<~RLe0*IZ zB5e+e|8|9mTH)QvZd3ZANqs{6vXfiju?)Pu$c0qG+w|4IpXaJ%-D)> z<-k8Qudl~suVm>~Dlh5~ew*cgNaL`!#ZY0=NTPf7P%qc3ce*Y5lD_U@=JR;edZEte z7Cp+E3V?(K7k2RUa@%N`!@0jp=}1qw_gIDJb!`#Z%6{#$(U{HXu8a(FdyBap%x$Y( z&)a{Mx~2sed91%%_E4kTOg@(!MxLX7FJU?>EUz{DrG2%^|{RMN84zBuS;Xw zg&Lb$xjL8|xilV{rThkV<0F5g)b_ zDWY8h?1uBSKbzmU`IHX+Dq+QHQ#;AQc(7sf6*;K?M)>+tZ z^2v$b2zCK3qF=@OZ^r>0*ZHVxGOf3A%gXdC>;_i`%O*@8P^26`SsCdr6seF~j@e`V z3|}IU)-Fzn3r3+0@OD3rku5)|K6->KojcV=4>xwm=Ra9cqPvwNX*V6MxjJH|ZzEe| z6*8BxcO-qhT9eUY@`FTgvu`KO?X)~@q#<dpq8T))s}G2l7=={0^yR0A?~i7_~6{rZH2_68S25j4A|O2U5VZFipJdy z0gs;qO>0phEiWb(xGEglZXoyP%^#01VYRvx-6lt~FlM8%;QcgP7oqKZ+Xb!F^D4>{ z;WW$Bv1-&Os4x671#m-Zo40~0M8mEaq7JQi#cfBOSDn9=h_OiU1t-Celqcy8e zU8)bKmFA+mQ)W4S`1v1(pO%BuR`9s^7y5nfx)|zmkHmj<)5ad)&P43v31=bp;BhE; zlCQ{-=iY3o#qRCB4)f7G`=8x1aSdlfd^Nf$)Hz7ElM7*%&k`p+Ub^O^ymrFfyzHFB zkWNH_cq2SK*u}%&w1=}&0j=zSgutI%9vx^ip^)gt?J6PMB`VXb*M?6G3=!ZmPL!I( zqzB=R)TyY^H$SB8ul67K{xCGvCsV7^j4OYS`Wiw?)FQF%I(8p*C0_?20e;jxc6YhE z`;{vd@%D$MG&(sQi=5Odp*q)4CC~Y`?yMIHPD zxC%bC;1BL3)rGAV{mlcy*P;17)qTQ-^-cDj8}S+$FSu+qK6j$%nlfhdWC*BF1EU5@ z#Kj1NQ)a0IHxVDz@5Ps5;u}7Fc-yw>!#f}Nd2eFHOi|d~zr#z|_Y6}&AHyfv zwe3<9)Wn&mBMW|ze<@!ROMI9zB{KT zb~T(0`=4-GWDrg@f8_m{Y$Ly~-^ptFX-%vj6>bd;3J+#YXcwg2-(GHCUE+_)cbOO2!DEoL>5RC-lZWQi2OEG0)F_Q72{AFww_Ve#K;@O_{}XUG3;p zkKnnO1Fkaab)c+z1AZ3kc}-@P4*gqsNKS%KrnLExnd7cU-@XeuqrZ2^J6%%|Uf8Yi zGVcVc@p&QVCRj?mbNV^!;*K`fwN$y*(P7Hbz>;swyW5uq!|S5B_#KQ*#C6zM7o9@K zTt^#w?;&7!PS0>q^H!c$sCCSFBO;_?_hogT$9W=0=;6oIecR-~m&dRAwf0r2_n2!B zqW^{f{u;znpW;6EDs0=?2%}x?->ds5-r_Nbj?{j#XyID(P(#qKdh8(lbW>qz*bsyO zK3SS<^uy*{6lirO2Is3Xb;6TD|(Hm)c3@_U%EQYZYS2*ZdxpD^!uF~9mjNH z=S~E$7wpJcQ@6(e6H6L>FCP{}NitmG+i7uns|F^>upPT3R5VzBF$7~nJp4^-`D7-p zSqE3^vaKs+b$`OeJG`I1%^~vmNbvE$8&nBbSu|evB0RT3{_a%Y!V#IYKv)_=((5DS zIXLnv8zZx@aB8fn^`kB?*o7sSgpt*u*Dp+6F43yQ5z)$wOQwK&hSJJ?B7@Cyle);T zH{w}*AjzAgZr+-3nU8ct^F?x*g|FAnkF@TI+w0b3M41wM#d{nuq4~|9TFrx^aTz;{F#&`HV?d{J|?}haFFO*qH;ZR3DK&FhyrBLx{G{2?3>psz= zz4|hv(ud{vq7Rv>8ve*uNAwVo65ZO;(a;)XOJZU(mTQ}$%&bYi?YR6PJ6V z2z;5Cw)xfyp%thxSeMYH^OncmBfO1B3679X{U`i3!@=5jrR)y$In8to9Y!skCcoRK^RIVg@We`& zOqM-bP8gndvdFK+_WJa)Fi7_H^)WCo2%TcfCLX$Fa8(9s59$yGJKgc>d0L%o?CXgQ zC$v5Zx+Go@MQZJrPcQT-=4nNUB!Ra5{!YcR^|19DT{(Cn_6YzI*AzLdPr^N$6G=P= z$B3BS1XS|U%4DxkX+K~+%{~=&SFgOQEh2LPsc8AqbRx%tdfZOc=X%+r2a1Diqql-O zTgyAS6z}GjF5|C7F)P|sj?k2BbBZvSzI>BhH=6l2E01%nVUlrp+fB!Hz3B3G9{-*e zT+fonAuf;QPHJSSRc5VAmm41!14;8k51sh7O+|m`J+WCX|QJg0_r8|SVxBa1&<#(cDXVt%)n-X=Tzw`zd#dXPkBugO9$>!yxQR4A5x(ICBhQD0Xkj$G zR-9kC<|~x@(jHRAlMYnWx{iJdia|&BJ!EDUFwI}!KOCd2M~UYq4+?ROCd7SJ5XR&T ziuCeFG|ZMpoNynnqZ{~t^R|f!1~@!aS+D#*)V+5+)&KwhUow(NNwQV4DJ$dH%3h^# z$S9la?Sv?V5QofTMfToXDciA+?Ihc=XO8uKoIdZ@`}2Ok>h-z&{{8*)y13}PoacCq z`~7ymKW^9Q6O>aL${|gnzM0dBL{f_FusiEG(Q|EUQj!(Ji{6?v)L#|Rv9H(l>D)*+ zO%R(yMCVPbzFc0XH=wcS!OTz0YZ`nR7!FwB5Hg*lcdK=Jx^Ot@jXa=5FQ||}0{88b z-xS8piu+_X*jp%8X?K;k7s4iT#42@ukW^1+i-%Sa4AR3S@z?onEz{{mpgma6_VV>= z!l{b+05tWi;XZ|vk88*Wt$LL3X~TZwzIR!d=+E+EQ}-V^bd{k<8H>sB>7-Ry0OsojGUmjs;+7)sV8eBEj=c?4aZrOBZ) zNy%_?Jin$QP;#=S2dO7yolsxk(M`C%ht6jbD&u?PI*}u83*|S&6ieDv1gblH@M=gP zouY)PI=a0%6fqrNIWIX{o5I7|vt8f3zg~Wi1Lm2I82bQWKr=8v%wSdZ)jl8Trv;^l z(ZcHcFP+hH#S4S3DtS{s?epdWy0`bc>=IeDCInQv~DxapkF`CQX<|M*&Dw)?Ty;C zwwruCFDP*8;|FcjYq3hm)PJ}?A%&7 zrn6M<)61ArZarZxMh3L`Bzi)0%U|~h5jektL2UDp{!M56-*ksruk(77G``HKq#q+5 z-Kn;#`_@(dYA=@luv+U<_P((eITQ)KH(6P#IUyyZ!FuB2i*VP4?L|##nai(uM2ARy zDc2N7$c}1usf{TlAevO}ck1Zwr>-Bw?ccW?O3-1tL5HOA{$xkHTa!4xpS_Xp(b;S* zWV@?#-CT#4s*-1uv}6_?!reS9aqwt_2Gx$W`6^PMg&q!Z8m9GksxPSYNnAHSR*e_& z__XVBv*qQ;Y~DtR2YllDSQAvvtEJfQRBmy-y-xmAYvqi(g{3qROz7il=iJI$yasns zD_J|v&bE`6lSCZkf0loqB73>o_0j=Vuz`9?D!M{kn1Gp-u-Ee57$CTJj+lTxiQ9Ai ze2;$hc#RqCOEsEur;3K<6S+y*<#l; z;Efr>tlMwh|42%(|HWxw*DY(mKWW1;d39fHf^y2@RIPbBF+;AmvrfM3?861a^h3xg z{*@DSf7J;wW1`=G2^(kdh@PfAy=3DooB2u0=cD?C%pk~xu%3CD(t>yv?aB)@49`Ax zZO75xI9$`rkS!$Z2Paiy)z?GRW7s8#yqxx|+=TrFtYfLRZ=s|ZFeNdaX?a|6e5Q3` zlCx^o?g>}hErurDt}|C+duigthxDDO;?Xeu35SpTdMEANdIi@CJfYs6^??rJvL&8- z5DC?nx?^Uau(MPq^utO?l7g<=J03_uwtP?HaOx=d=pcP5%cnL;!$w2E?0Q5alxIy^ z3BO5?m1HWc=Qk{wL2{F8svdNcYr8tw#umjwzm`m2y?5tnlq7q(V35(wsA zcC@%sg=`_1asp1p!@JCXT9IB0Y2iwAKCq4yJa|AY;uBOwvIQ%V5-u z=um{J)2*4o@U;>lE8)AwKGvH<1cGyDxRr9I*%AuPpW97p)rmNL+l3+Dic1u+``F^R zH2|HlQs9Ig3A3_35uN(Z6~fgyMFl0+sWG=z^)TM}+%GKKBdtklIkuz$HE%47Om=Gi zh#J{G2q}5qFw4Y!s=LGh*dwSx))-1f2u~@ zvGdYFenUe(OmJdAN9>2&KGro^N7fc&@=Hfm$1rX-+es3BXNy#P&DVrMM~3MF zBAB%{yEaXivCn(!MZ-faD8#$RI5HQ=*w|rQ>{oDm=5W;5O+BI=d}3 zzkl`y#MRQq8JY22>UMo2NzOD5wf&!Yi?0m9m4m`r8P04A0zv~fv-AOE=BAm@o9cto z?BsUP(`YQAIiHa6Zcb25Tg%&Es>YP>*-!fZb_Tur0s&i;(wOTqb3M$E^>q?9p=%%U zW+bc7s87$@+pJV;*O-JxBwI@!cE`A0jEgLxOvW*Dl9}`)%?!kM^-0_nVb_LwAAQSS zuAH0tf(6o+Ll~#W=)4&opQm{@Qm-f_t6t|BcEhlB=QYW6WV>~)tk@8*YDp&DlI4Pku@GP-{Ln{)4y#OUVV^M z{pcN|^1foY4x-KME{9~*!kSK$f7wOOBKO>jH5r*@Rz@i38>Kv2N1f028}=s7<+`?S znbtDHq6LWyA9l<798Q+nzTj}??a8wE(QMfw#vGxKam!(Eo*XLTy0mDqYD;joV0)=8 zy3DG7Y!Yq$c~OWFb^G3{gt4l4VM!81~P1v>$h_pRHJPgDylooYae{_IeJw+?Md$6Q8%N|A4Wo zH+IgS+zW5}n+ucF5aq^tMD{F$4Xx05vF5sD?e?Ed`QOk*&zGfo?iYM3R6KgkWY!a6 zIA^(Z#PmSG=@qZ#tGT-Q<-m9Qqrxga_rM1zR;AI`b`PE9SD{t-V$N1B;5 zEL~^Vq*3soMv!=C3~f5e!D&&<$3=oxFQ&Vqo*I=hGZa;!FS~UF3D943gIYiKYCU6| z5K$Py-snG!#^D*l8Jam6m(yCl zw(p;?r<_TMs<%>Q607*$KsO{_<21=(J(+7eQ5<^tO^}sOz^2*folAL56GLZD!{k=A zIdc7v8!n97-s`Aq%sb+1Z0xQm!g*8wM-Q6D5dO6lHmv9w$nIf<Z7wRt{E#@y_{4CjAYiusWXbUWAH!~EEu6|JW{0;J zF~HpH>7OoJXvSAK>{9Z4Sv&&WM&)I?R>QC-;URe^NDtHoYeqC*njwkOmapTz)rifWBObSE zJ0oEtg$OvUNN2|O>WSrwu6`1Ye(qNv`QS07{hz2dvo%M*XgY;T{Dj!V=a%Jj1+A5x zgMO7HEnQ=`5P6kC72Ff76kk0*Hea~n-0|+Xk<%l=yRw3H16B!HAnEX;I@D7}D$>R3 zPQaGWOrnYA^F;pEfYszn{`UCJ_PM@bM^}}`FlwrL$QbA;^`(d9-@?>5n76NrTs}i*AmL%cif2uh`@MIQ>KtUyjZbLeI z3bI;-%QEJ~BspR?P<+-ad%5WjMy9l;ami53TN}$E+L3g3+Pusi{(K?+odL{;Ow*Ta z^7o*_77i#CYVTnZU+LtPat})8_wQL)_4}Wge1Y^ISNAX<3p63@9|Gf+K@k=DWU43vczjKAl)&;5A(@KXY6E#gFZhfZ@=%bLr%@vsIu<{EMN zAV2Vr;_nEd^B<8=Brqe`_(N=wC)Rk4OYMMbub7W+|H`t=T0_1(7@ zZvKo4X;+GQXT}?M(~h(>;PpoPoi<$wbDfPA$MSE>Mt0*9nGe)EzO0B#92`JBa;dJ* z)X4{z+nAE3niPV(Hp2Lu;TRiZDC3=(?jCE#Ag^#8$R}v?iHUCW$qwC)5Y$W+FHvwA zX#G>jFsBHwePR2!IMtpW$tuCKTTUeTYMV0s4*5uX<)5y54dp!cH^0B{qnJ9590)#| zt%MmFf375L*^A^Vh>bpd>^yFsdq{Y>Y;Dj?(B!Std(vldf^#JNhN4Ts5_5+tIDn$jgH6Pd zpbK2&OX4|AtPvBRhRc)EZLjEM3{i{TD!K*BKPtDSutep>E%4hy?H4TfOLgT$W4k`i zyE%l&X}lph8tG`={qyJ0@pGidWwy z!^cvpA=z8sOD{!h&m1v)rD^x>fWt{RR=B(K)E+=G1S3vfC(cZ;mc|@-_|Hy4?A$K2 zQ`+!Pn)b+Ch8=#ec{hxqjhur*BTc>A-6 zd3_>O91k+G-6=+2! z`Y>eY-j_Vnle5u7@V(A$H4;K9m9B&j9Fv6d0VULXRt1_jmm$pS??!GIn-kekK`Yu% zs~%DYWj2ef8ddLZwrw&pR6=UpKVv1JVrB{Amv^38OT6p1hs&&mzY2(c<{Ix@9?)0< z7n~TH^!lWR-Q(uCLf2y%cF0lOqMZ$;n?RqkJ!toSVc)_K*=E;d*@0e1qLGp(dvkX*p>2oJjd(4fJu6lzAJkC!N zH#BeNuJuQ&(pGiOc~AEeCZYRC^Djj=7)v^#zu1o@SK7vj4HXAnghnI}c=g}v?^qR9 zfrq`Gat-cyg$i%W$zmMDx&RSWJeERIyP*Y6977W!0`e>41P)K{b6oA^Yl z*QmU0^ti>;Gju!u7nv7DZ!vS%%7-!5`>aIVXW1L&yo20MYQ=htXILgHfJzVh z`TL5bOGcr!>t)k6xukcHAyy_x9*>4`gC*DSHXFwIOFpx16Z=%x8&DF^hc@1_V?^HYhgw$gyhoFmQg^4~9P3u(U%-!&gs?MbQ}#_G`% zv-s!1j5;XBGYWR`5ENVkU*w|K8lE5Di@BCZFgb(t9%$8tOUv(Z>6EN>u9K|UVY)-w z-ra4lZ8RoR{H;T&4{zEeeZsE!_`yzbQ-U$IN3l|d@_aNU^+*VpUNIG0EHVX4|*1Z^=*id#$GG6h~o)s!_QT$mR(fp-8 zjSDs*DJEvA<$Cw63pb2X9Dw|FQ^pLRabDsrAA=^vgbv6m*XxIG# zx2CX-bOLYlI@Y?EKlbb!??sd#y?a#Alw|o6K;~T8vxj};ne=X*8E@wQAa`OLpye5E zKHO&k`&sP(`J%qJhnn=7SbN)vqMshl!@_h;5i_E@jiT1t-qn75ZgS}^lu$JwvF8^Go7}@kwBE(M{1}=PON)ss9UKq1HtG7t zu>7Sw*}W~Il?!$*;W|a{Yi~^qSYXP-7iwn{RxU0O3QtnG&^vzcq|Z3i#+!Z;;r)U-u$=mv-}L z%C*Xe@yh-vh0|#T#_V{l3+?Z6$HV&^s@%&~<#W07+NBwWewe(^e(4I@l1&)m%I8B4GDcqI$fe4 zxt)gJ1YW04au}z`e464gL3^v}b(Q<>0V5ZzK*B$@_OuaW`Fno}e@-TGcsK8Gv15sv{(md6=%!M+JYf#fiR(jB{us~P6**UU) zMA4h+myXO(p>6d2_T|q!F>;-CiymoQ^`-na`u(YOIdw79m8&78A@!EIEaKz0Wue7c?s%x&Yth{dSLo5Ps3C9W6I*{{cx3Kwq4 z#tZA=HTHNZX?edi9E?Tk%|(ZuZC=OMx>}!_(KT7sMPoeC)}zPamVw~;_%1KynU{J&1HVf9#3JiqzoTK*EsCzjnLRigS9ma(h_>s z+vQXE>+-cC>oLne&$si7aJrrEj?PQTklFf>SFW z$IeT~d-)XBZq|NQxmkjWF0ToxPqss^>T1^Ep^8i~_eO2H+KR?vE{{uyhikBPyXn=% zcRK1#<*Y_(-F5D(iY}kUnhoU_Yj2qhu+U3lVG zNy(tZQwEIph@rjYN*L4A(mkypLh6$@ z#y^^<>8Ytem6x=fC@NP5;3@sYUYkm9bD_XswM!-Dk1Mnt^QyV~^JaovR0sNE#F6o)5!cs!LwZUoQ@rWEVh;?|Z~qPf|qRew)IY_z7_u z7Z(ZLU{@lS-tVz^&#szqZN-p78oxH$miE~1yelzA%&%f;&oSoFg_fU{EEYpZOjnm~ zBveZp|65U5t^qcP^rppG&Z_UW;f(i-a!YS2#vwhJuiv-d>y-5AWJ+c7TXWIyh1Lw6 z{2;oVI8(dI!}Zo^{ryv+{F1FtG$?eSZhu*viyfvuVq=&4DRXx{dWYfe>+>~s-F}By zCd?EHw%-IAscw%U)6nP|*1w;z@%@R{?s4nJ2`@*tmKPp0`4O_cI4P^b{Aq>gx4f_} zm0VeYX`}{l55NRjVIr*ZWg$=a`O1vTE!0h0)0Qa)AMWa^dz$O+VqF|t*9Vyl3b&oAJH?p~hG_b}&~ncnMZm>HkYQ4p_8 zYp3UV6M46?6f)zrSD)OrJp1=cm=QI4=&|@J=bU`c&w%kHHp7_*8Tf}ZUZY84H+rd|KpzcfIUD~mv)TPZ|Ac9 zUByUw11?zUeB0W8Mkjw=#{c~8u^D(B(FeM=1UCE6Ga4y|x9Hxx9b=j6yJYZRvL(5&u^p_rI46eu~xvuWhcbAd&K)Kck2{ z%63(b;}3w+e|>j~BRH^}l{g$-`kT|pzh4^@JGkJ;FABH+`B^_!!@aic8b_^v{tWX2 zaKVqRXXF3-5C46F_;#>MF{L^hXd^HD^Jmh}4=N~eKmJ8o(lGG_NAQ()mxor5A3b`+ z%+1YR6ebSaYXgTECs%}S84dg}lyQes#hjk~+?d{)f~{9Wfptj1yX(9%0x24iJHQ~Xb1++Tk^^|wasW%$ z=#-b0M#NM=WHbd_A;6r9Y>G!h*2SuU#Sb$-KYx*m$Ee1b!Q7mfCDCv1`LU{RDw+y)YeId+_2mH_;^UtgUyYls|8>?Hn4iDgVVGx?Sc1D6h^GI_^D82%Wt=Q2Ka6OzzKQqbMSVhJScjn-z&u@rbee> zh&#f0v!oOWm-^vhCl z=EtElg7?w)M5f-g`AsD%m>X!pWAOTmrTeIy;&=!F&Df8Ne*t z-0-9O;k7xjnW5%;{~eIwMtX`cm|?*mTO@GLSA_B>@HLp5iviBsT`L0R;H>;!kqkf$ zx|9ST*LxkhdI0yjS>R8xoiCf*I~;Zj=u5TZ9s?@PZIWwT?{O$ojpY~6OBm{+6^FpKj?H9Y@z;b0*7WR~`R zFJhP22LXgv&fMObACx-4DKZg|g?y#)C$(QwgH!g~_+&elr@*DpJ?G>QeaaIqw)NIx z9C$EJ+tz;fBbfz;&>z5=l2J6yFHG1;=IVl&lateA&F<0(&cuh}es3s^1@8c;mUlln zj0dD!)}%Vz8GM8A1_>Hr`+mS|xdZHvE&(?y5z_k>afyj^=0fA?Tm_jhscXNl*uH1L zC_8)C1jku4WygW$zA@drD#SWgX?LfvZYx;))K8OTbRpZ9mNaj|eI~TO#>HAaMHaF*oClqu z(>>WO^r`Nw!Fi!miDvpW45irF$F^_wr$!6d8-4OQ*-oyob3hK{1)BaoqnP4wdseoh zKA2HWXUTgrhcnd>%M8%uC?8;){eYDR^|;bjAJ%ZT+pQnCjj=SIU~^03dn=LVDRv~W zih;MSh6@nK6}}SDg@{uC>neiY$62nxbeOtTR8`Agp97Y2`%%QYeNuj85$SLN)&4pOE#Qvml;Ff2_( z&kVdfA_mkYCpG5*B+ah4AT#rd)*r?h^M$*kj#qfIaa*G>IUaK20lSDE**CrmgM7)g$difa3jJxizm z3Aizp=SZ>4-WvTCQ?BDSe1Q6@9ub+C*^uhc_khY{d4Q7MpgocN062EQai)z>YpsOZqC}spd;cdvBzf;lZ%>#P%SQpX(2KR+~!X%=g61 z3Pp54FEPA(5SS*&1PWroJ9&O_hx^T9`tU^Enpx8q_JlSMQPYDzty|mx4>y`Nh$E#k zL!}^f$D%9Bp0U8SGrBDJ=4zp|UtS&;%>CA6G1wp+fr2qlD9h)GMJ9WJb-tFuu&9o01>!9YKzu3uq2}B@+6ZyvRAK;Of;`k z^EQt(UoZljiPN=JJNkaMu?fu-qYUu=c@0FrK6dU`%yB8PB7MG?Kn;u-x2`Jgh>1gr z{{X#YehAi*{~fR(8r!CG1;(5uZtDiHNK>a~Tqpv#PChGKUY%fBw7n}tRYP}d_J&My z2W&BIIDXHlkY~edz?@M7e$1wgzDCz304@`3(5k?&N0fiu;AA?G@j1(D31FZtKBt5c z6V$n4XTjnXBJ-NwKVuP-W&nI{suznC)cT+0;@quZ3aqS!U_(Ww0Yr)i+4Z|krHp^T z!Ah0j*VU`H=R|w{nur}L-?Ki3Z8^_d2ktgslH?krE@Z!Yp&fuM?|_v(z1I(vYd$cI=`6Fr0(^m;=^8 z9l$1^b#!!eZ~W>Qlu=yW&vh+9I;3Ar9q~?t4S+fBS-@F=S>fKwQacPlR#&ST1YV^K z&jH`^9sJAA1@FLaa}^^dPW1eDqz1V{n$&doW0p;$$G>ux_b<>-i@=C^gduAWe`SeiB@lcTj0d)shN2dPb4=Z z8D?UQ!`Mr=Ux0-Y@T5gQDT?MrG(3y2=yw_pigf8WVrZK8}9)yD50kuAQ@kT(| zi{2tR>9FP9FC=%EXI(xMP(}R|dyNcy`G>j24E7GCwAKb(G{#Fn2Hg94lKLoa!0zdC z_EB5`e)$?{t>1?;7tOn3r>U>C=E*}^T-Z_}>vftt+=Qywo!YqN!EEC~!a0oGs!6hl z(*h`#o6MSPEM{0_)AL2+)gEQ^^H>S5nh9WY?Oi)2P3WaL100jNHa)NmuT=hU8*lK~ zQ55+R^18}8)V04><)}+%>3w_vbuZ0{%A%vKtw^%n7NNGB2#4RoaY%AeiqYko^+jQx zne?KCUjS&61Niflj^`X&W}bqXW~<)25=DaL$=$vv>iuwgNV!LQJYTUsOl(U*sEFdR z;R8rd$~M#Qp4uFrKsFofF=-+Adz21VGw_%}>~s?(1g2lQq<`P~a1k;tRm_S7?}cOD z<>Sz=|KpGUi3$Q=#rJHU>5u>VOV4<$0wOlb?V3KT-=EoE8EMCTuy)1Wbt3=gkNkhT zDUi>q#EP8%=liC_rSaeDY|C&d+#eI^@mvC zUzel70$ygvo4D8i`s&|fQ6>#uAb~yZ!pQw@M)}u!B4?ujFZ2Ij!$vAJc=W};-jmcf z%8>@IS}y&X#{gz+0upsc5S}UnObEvsai8OFBO+@MHwAX*=4Nr@BIi~LVjH)^aXqcl zFzZRONp=TY@@z>b4;(DkdM5x_rxZ%_?bQ5E-0mHnA}Jgv#~=|v)xxFxazEq!HYlTn ze4~n@JM=zU{41S)a9O9$z4+qwyG(05KxBb~0Cn9F5F@^EFI4iz?Sh~Ct&MOFik9kG->y<~{{|-_$9)Y(hyLMA`P+)Mz3P4m& z5o9qdSg^!?2i;0HR>v!;NFCq*J}-Q?IKX(XGoT*(wXJDnHG#xruDF$fvn)~52sjvQ zfo1+}1Lb=Kj!^7EPx89RPV&hPYqnOXPnbAc-fv@0s`^UGLfTV(GMZo19F42KxNId{ z@?>!|1B45Fz`Fhb&{s@=eRPsd4WBuZ4hqr-l?Fq(63iuknP0nUV8761K&6^4DE(_q z`)lTbu^lB3#_8NeR+1ZXQ)aGrmyvAat&bP7ouVV4Eb+aUQUpxTLlw^uL`p{^u=3ONEw*lDGbv%xvPmz}dHYW({bo z3ds^)_gObX)j%7K5k=zY5}i zR;=mk+J$pEg_bvhu-Cb-@~T$B)1`UL_yxeY&L7OWeAHuPIe3b*owMonC!-g2N4Y$B zjj#DhB?6FvT3_(<6JBEA0-m=SgL%+_E;H)i7e$4dGj`0(h^PxdsP!$Ox(oR#_?|op z99n#RFIv&tz#yqBUa*s4O1=>|^*>&jSsN+7Wro(u*Znq|9ZE>?D-r6e6#kK6GF)i) zD%ST6Fm!EaJlP;prx18+KcFf^2~O1`s*B-x&J{#~4ycGP~a+n__4BVzlI2Wga|k1q)J4>wOz?@c=aKWdLda zw9rkQ$<3N*@3&Z_+Gh!@qIsJk4`QCt|9;DC*URWAW>~_tqx$ z7O9u-KEMhb1Kx)TfNl}ZvU|{Xu(Q4Y?uPdOVV=Ugo7)2fn$>%<7_0G${MrH%(ZX*y z_H;W0yZCVqXT=yOik*e4uZRNj&k8#{I{G&FGuyL&4CpnP-0c@l82oE;%A}A5bnV@p z(e%|26E}ksjhp3qso`NJ)3qQXNqF^0E%$XTI2j{}~XfFVZ_b8m20)UAi zZ}WCo;Hd&4&xC2Ic+Cs4g>;N)bqTtTye40m9V|ycgG;kx7FN_B98{qh`3YuLHy4a7 z=-s@0!}=*E{SAZrL3fvO^>1vT@b?R@oS4166&)MG_MM?tc3EO7LI zN}qWwZZ#b|_q+BA@|8_n^+8@tLn`g6SH4V%FCqOHSd$H8avip|Y5EM=Gc0m)ED3NkMV?&&+8 zgx&{ky?NrBi*LHIEWU)y0K%v37VdL2!|2#8I1FZdy!e|45$G03aoDkD(zdn*$7a6U z^Ejhmb{d7=8y*5iDHQY&#n$8D>9;uEFhm(LK%wd_OYsDxpna1h+%b9B-9)s<0{y|2{3;IcZ=x`>9`DTgjABYg%q{@|ns6%zPg_IaF1 zoW*BM+;vCyZR0KCcZjx10dM`8)ejpLCPSwH8qn@@dT6*`1#}?Gt^^`v3l6Nwa|q;I z2$UBMQoJ-qRh*@`81PQzZD#(qa{sqBGBC36z*cd)$#_`jtXC~lpU~y!`_~Se@8ebf^XCqk9FSkR*??)71Vvj_xKH3>SNAsfhfD(+?tcSOj z0LMoqu$u5{*H!VWeYazt{vf1ww)p7o#ISf*GKfW7p5HR%W6?3mxGjpR@C2;dgIpX` z2JWfmlTZ7E#KG!#8ynCj&*(K9momz=z-6tmaL4B$qz=p_$F>`R!lN($5aTIQ0=?(4 z8pnXhWu^GCu=!S&g`gji75>MUv)J2iOcC@ruhXsvE| zRrKgR(lEsm3K09WM1ZNow{w0y+@hh8;_?CfHXsvlHJ89A4U3LH4^gk3NUlU!b2u$o3qH%5r z0vhqRl^hBYjNk?>5BsOP)PERTy!bhk#*D7gP1;Q6+F|QoI~x3FOqTUXb*Vt1KKM?G zdZsmN!*<`ZU}q6QLBUV`i&8QwzQWX4=lX^R_AAA$*biX5#fX1>xI=N+;2#p3z0I*G zMrIZd$OngVui=Gf-ydu*KyvroK=$>5#Os#KxS6+e7?Emw7LJ*jCd(s|4T?9uaCyxA z5xd6YimpVFfs@6$3!;sF4b-X4GU8wnj|59L4jie8IIIVefLk2FZ&9NZAXe5zynoTh z@f5u#w(h=kJGHllj||DCNVyUs6@Be3gA5yk+)f>SpA+LE#j)s830#0J^)nGdRtl{q z3{3G?!`60ooNFh5nfi*y)kM!S?y-l-5tk1WP?fB~2?A{W)lTNr;+|!C{t^gDn=t`U z1`9`I(r+5KhPhNiNXB>z7w+0ki2=v(w{RPuWI%1%n{BzBZ^${|aWgU9PI@tsT-p+( zrF;M?&|~IBkq#6#+oZ95pr8Tk_}Td-88fp_<_O9^+}Z!=ywGo$v54ARmU!)XoR*z?yikdqj#iKp)wrhi0ZysWQY%`zH zD;`Z3z{+Nnf>@Y;iZ8koNq93+F}_?aliv2+JOAw-Ni>B*uj(!NuqXnOYi|kh0iKkL z<|$aK)?NZ+MJzPQl=Q7|HuJ80Be4U}f)NN?9Q z{Tpm#{!ngJQU!zXAUB?wC4CN2x1SDI zD?u}l06$sSG7Y)?Jd?#pQEEb=EWTw27UVxR#1U{|-K-5_v0XpQAHB#L%;KFVyV+m#IzQ5rwi&)c(#C}{b1%#U%y*#cB$lBg?MR=9z-y05b@o&`ql%K{*QL|YqA-L4e zUJBn&bbi;;!;Am3qOL6v1D_LKb{_XwKrE>J$n(YDCWfsZ4f7tkJTfxZxiz_f)7zp3 z>K145$idgw)>D^EF!xxw%$zV+yWZ_W*) z2I>?id|_F#r6Lhf4yr_7wBJ&>yjs-oBbZZ6y#aHAt3TjC7`Ec`5;gjP~4u_)Xm4 z=C0jj%@eIc0~?|nW%NMBt)ugpjd&n23v#ekr-ynAF9n!VDW0+pf()Nlr|!2==V@S7KA zS2`jlJ!(zm*T{sj0H;f~|5Ai*c6qE$_1mPY|J0|`@dzw2P%9^{-`>i<9|}k887Jn) zMI>@g|L|DBU9c&G%P`IEF8;@RB+oL!6Xei=S@QhPgQGMs0&#zd^U@$E@rf~#i?VK6?ADttSWM!&e4_EgF zl+cis(NcLFzN#Hm^KhLupu|c)fCbBWJLpzec}cGw4PYid@(r~_> zHm%J2cPhpxMTfsGVID&#qFzDs`_elTiC*KlWN6)nmx^rjobLw6Zl1dz|@PqJBHRT&l zcLDu8Th8P@pz@N|ylEl2lnze^g$Ccl7*s$gt&oy1T}9wI(I0PbiLG+x8?!uG2Z7pr zoVC<`Uzi8$+wX(Ff`NvNB0$_`?ex~02H@0a8bQBPYOYZ1u4jDvx~J8dXB|=Oe4aZN zr50UV-#@>e>)RRWt!SjS`;u8IP1D*76v;`&=|( zJaNusI2qIc%$QO^X?H77^60Zj;&Hz88-SF^?q%}3#=!ghjm2H*!pnc}PsObTs$eb1 z$X1>Nnado(j^YV5zggQJ3IT{hCm;DWewekUVDK$?vU-@i?b1kx#WBZgp$*5 z0@I+kCZv7f-J-|&9qvo?1{?-TuE#(YD?fA%Vi;ZzeG7Vh2Dazl3^d@Xzs@6{7R!u2 z`^8Qp(mIR&(7h1#n8{rwFcQ^u@++24L-h5jHzYeDMfNlxG%CFgKV$9~@{F~wq~m&G z09x&)=w&<0+u{XemFeoG){sYi}L=!TL^O zIqT!fO9V3(bc5}7mA(h@ItbK*Au909M}I2x-vr`0+s*6WaQfDLf-+-w(4Cd^1IL+O zSaM&r2YYY1cxnmZ)x>lPJ|coEWiwttc6B9}LOb^o`*+{)a+WbfsOYddqSTHhys%Z< zG>ce3;PXoM9Nq#ga1>rxeaz5w><(R5B|Dhm#ze%r6))Ir#>$ElauKtJ-Uk_gBqyY{ zy=q6QVKJ@-Srve!7**)S+yQ9P!~8Qg;VkqmihRBM`?=fWOaY(nikONhWG3#a2^q zd-hLD$rapVxWV+Ra3zgbU0t2Di2>atwv9pGwNHRzRRLKnf9JXfQBOIH9twl43cHH_ z+!Z!+MD2#2BYkY)!RiIbQNG)B76;W%3&E}y$$&M=D~+E?w(zs2;H)p;<0&7_Q}i;u zA-uk`dV%!3@S(`6_v!IIf@5_Z;6D0-5ao3&HnkP(S+6JV7_LS}M?y~lA9UJM3qYz? zw)y#8$lif=idbj~XkJ{W?EdEWLQ~TBf|y?n~A_F@_1(*dj7X53#o`n(RBDgcBAX6?!j8^`U*DD^Qb}a z)wIy1SBB8;ZQ6K+bG+?J?Ky_8C2*XnaW3&zz#|#bce}PT)hnbd;BLf1PovDL<@yNF zavq$!?Af>|PU&2a*!okVe;XwFfAu=!;io~$U;CXuTgxgA@hl)Dy}Bq5ceTaP;vwx7 zuzON~akXmH1T_COLh4^?io>CxrP1^5c)`7rlW^BCOs{G@Qhfhu94MILRPntU*Dyx6 z35UIs14++1KmhOYzGCQw1J*k^O_+Moeo+ziEkW}3)_FMb*N$}J?uZQsUz>DYe6QAL z#~=*Emrc3a6|mi|nS6U`Zf+jpEwGfpg%hT`?ke&^BL2{59wh|=B;t(`O5Dm8{EBSI zi3v`IFlO=E{+?UdQ1Y4>MD!)BE1_%O?*%^S_d--&bcV9}hpCM{E1=)-nByHC9mO69+CLgO@wXA;e5ZYi>kPUUjq3#| zwJliOP{TD+nRb3?|6j%B6m+?LXgr#`vo)V!HwM>(9Q^4`w1a@@uNIph=VN8hJfIpt zx~&clKeH+;HAuz|2f4uGa9th{ixgQ>5yb^wu*)V2k&po>AMD}@D8E1+;5E9fO)QOG z6CW)x|5$#7KB~9!M%p=ov511?`;q9o3S-3#$8vs2ANf;DH47QE(E)E`A|Ez3lyBmi z2?G0Gg_6-e(doYtw!;s=lm|fdTCKe5d=mV>O3Uv5p|p^#%oKmKxLb>h1t}j$C9esg z@p`KXyE1pWw|lEeaLr&rt}Q-brG1W6Hu}K1R3brIsD8`{TPbdM0v5JTh97KLVi@|% zgEr9rg93t{ZWQy97%5I+&kP2#a*dsUFfA??iIencD7Esbv>sN&E%_;-Nx26OEV*iO z$I3B0l=P16iXc8EOJtQj$Bz?T#F?YWNHTzOb94b~c3<8)HXFQ~!6f((019=@acTq| zZmT?hs`)VMy%M?-$*trCb|Wd@R}z~ApWonWKFB`g4i0zc{X^2z?td@@a>Z z(TP_ymX^+A$Ylc=JhLWw5lKn5JNY}em=fL{Yg6~ca}-CqNfC| zRH9#bFz@U7#=8GFZQ?-W%y+uYru?1O0{u5PeZ>fw_Pr4 znA{Hr<8HPGt7T>_e`+zDYF%&Pw(!h5{s|9azkuug#Njbq(C6A26w=n_wXTmb^2rTC zf!)042LPPj`8!Kr5Gp7=^4Uos2a~Zz zz@oRwiOcChp>IX^i}l`A+12(ab+8NeS$~2n;?(hZ+dW4`^xE6(_ZNSQ&7_(@C~0I= zggb%BtS5!$!Zl(-hY7LmmOd1}#KAzmi+y-Mue&)yu9 zahN8qbuLi+?1h#E?f@6E;Nv%Su1B~o9iZjRzgL+jB)zzp2^!7&KdI98Z9Dlf_% zI+WXWLrKH^xiv%DZI@%L%cifvNk1Bb+#LE9nycTVS5Nsrt0p!vWf;T&YSr4+3u;@LUdP+IY}8anA42`_qEwm~n~F5`}R6kHg0jHt;jh87h^dVs9$R1R&f ztHH-%3$tM`g=3zynS&jmal+9rzU<0T+?>T-dd^so^EtyHK-CpwG6S_!pPFCl_Cn_% z3;XfF9*ItyEB7Kf=Zy!N^p-zqe@UodRK6j{AypM6$rQO1-Te~e*l#|n-OSE+?nog6 z1v~pfw1eq6CbLVUaF(f~`k)P}H!+!ezv^~Hn6+(G(U36c)jBfO*mzq9+qK)~_{Tz> zj0CLOc&QxhCK7qCav20%Byqh`2`k>ZQ;yWE!%X-089phd75U@rvxRrd;!1OU0%2}{ zi8l}t=-KOdYZA`_YS8Fhy>eHci-4!cJLUAZ%5)ZR~emlNgok@F$IBuozu0BAG@slfVLx6{-)7O4v^Mz6>+ zdevJc@*?ep_UeA2|Ge5@z0Lu@3*FB;+?coRm&5c{4Y8e zJiwPIMUwuD=tfNqc6l}Iavbk}f_1?10BRZOBQLKUrUei@6MZ&YvqdVuhvwlb>7ygU5Y zUw_5#40~{AW_=DP(W8|vyEkt=s1x19C) z^3EKxZHbH{!YY?s<5#UlP(j$iiN0TVuwaM*A^Pl~rD*Pr<)2dD>+r18oHU zgIiC(iD&3z%VJx_mIw=9?}1D;9EeI6RJ7jR`= zP*8qw-&+25F){830&QRz?I&9tqy0`JCz@OHkA>Z%boHwqd`C^YiH*Es&6r!3x!c0F z#5mUV&&ilh4bzMDv+?z98Pmy#V`OnDsJK{O8z*(S)tq}*Q_*Og4a1w-Hq*LwcFe#7 zj(AeecH*elC@6d!+tCB4lzVg%RX&Rc`@;vXfsUchSR6(U_5_?rU{MhIFXy*{E+5ME z7HXHn;7S6rly2gbXuKwAR%8$B^;j`5EgC#L@LLrU`XP(q;&|y~JR-Bjp$fpv^q$&Ff=j@qLy2Umg)il!OBr(&vLZ?iDY2OnZ zS6bc`Fj^xL@)aQxx$ zcT@Dh#We2w^f;Na!Q!Yl`^Ui&H_4rT%F zz4QlC2R9x7wpxULM0-9&VE(w^5W;AKk}4V@P#q`7#$L?h014XZ>py1IxViKle>UGa zJLO;n^c8^$`g18rs}UX(W1g4UOw1HFVi^tui{fgG#wyptk4M_woIsf2Q)_E%Z2T3y z1$LkpEmRaJhjb?FMdal09icMVRmTSGTO;c(xHX~Lv^LszWc~CfaHkaXToY!oS-69h zgo8U@eTH|3@XY#@UWh1rO+HtxdvdszO6-G?7AUXr)VZt0#lw6D=2O%?vb(BC7gGwS z_DUp7=ogu*HN7+=A5Dw3{=k&e9iltuKo3MDwpDn4iA+E<9|G6izFavadxvV!Tmm7{ z3R?IZFQE1Pj`nZmJRagT>kwZ(^#Vp9$HX(bre`2#6Q5MM18AOjLc<&gmj^Fi=+-STvbU;aG2*v=lA`fym?TlO4qAQ3}s{4#TzT+2f{(PpYjxJm2QlP#U^=p~ubR z(+i5_ZVdyMnU0tBYeU_aKEdE#_&R~9S!h;#UXes{byQ{T*1UF~OYgM~?8y%ZQ7r zzJkN;`%qPwL8sWktt=Z>(?RGu3j9v@Kk=}`uri(^y(8nt-EM!>RNq+6HN-W+{DwII zTQ|`dfKPUW+TG;*j<~J=c-m-s$rw(UUN5V(O7tPX25`UO0ipa(^Z8R}?&Q+FaX|#| z%ZTb8S?=}sffShAc@GW2I(A{8bKb}O^d@#IF)T97+g{jwG;>)%@%S%dhu72z7+DQ* zyB{woCCvAt_s(&fUuBAamV`S)e=H5Km1VfyL6v+XSLX^*z}4S+lrM zq-WPk$2dqGaJA1Q$Xrs6-Z-T#!Q`=HJo3?J{cgG_p+~_8V(8MdghODR!q{9BrW`$O z+OK5ON+hONtt55HYyCB;d=9Fx0zQ@I0|$5Z&)mv3O8=t78u%-PaY^pw(QtCsfnzD* zN*F`Nq%T(FSMH^{aYsk6v2~f3yehfOYkd42o2)9^J+aeX2@BZP{e|1REemnO3vn$r z2`ipG+kBHhM!&Xt&S49BRt8&FkZ$1jJp)lKhrLukYB@VOPzn6)V_&=&$te)07ASOn z4x%M8$(%OqdOHl&v_`4j_`}kJq&;d=rG0_}C#RAE;oZT7WJSWG2!0~>YzF`af-J6s z3du9f&VFga%XPh7ax;r+L{B$c6&nk|HT#Bd-jC$OYEYaz3;T3oS~#nJ(nD{_bml4_ z$%}|JquPvxxIhxaY$FMHlT~Te#kG4IEzqx6yYM>=EL9!+F#F^i&cSxJXxZ;78#Js% zZpzg;P|M;95{-IBFX?{R+r1Uf%UNxrMG&~e#~DPmbEu*dnJ08=*}Hs4OoF2A+zwrE zB2pu0C8pe&`&3ra&o(G}6#KtT$tMojUZ(?D3HGAYG z*1{p&KNL|#qu+&r=uz-uq>(pPxz8xbTa^|S*y=+LxnwSE4X(kgNg3fXiU~r4W(|pe z-sn(8@`{ebQRYS7>dy*&b2j&c%YS?b8W(8WOA{viQX|nx^O<(guK|VNWjc&<@=K8l zqb_n%FRdOB8*Rs-GlLutO%w3?BH}73PQ#t;lsxlw@Gh8m)hmeFxLN369p?nn<><7+ zEDs!_&16$2n=8i$ePfR3}1Z92cKolB~P9nTs2U_a6HUPKI#zQTsIaK4E*kAJxB zdKHGIy=af^ADuS(h6y^z8S$t5ZY~=`!=|oTH(%kh5*N<0gQ+DGTol30A;1v$b%XP2 zJlW8Gc%&pE)N;f`@~cm<6Aw0$NxRHA6b-l-)j04(EBEL2rqQ1}Oi#-PrMfAg_DCdM z{5E-@6L0Z7j;P`okMu@pj()>eA2r_2{phoNbu)>AAH0W{S6rhOBfSB_E<-hR?wmm) zn-}CJ`8XSI{%+m;yJ7nMm9&)idE#kinvXoP+nFh!=?D`s#oeN*q+b^kqRg&(rxAA) z_nUxq8&l95A?-<*awA~Ksdr!{=5gh98>~g5aUl(&$SY7tmkO|o1W088E&>~IWNB8tw5GApjV%7DTgqYKE5*yE3 zZEQie7V%8`qQ~m192M)Z%}c(LrA;US-bV(5Iv(4r@5f_?q_;@9Zb@!Y0`vFQf3Eh# zv)}*3ERnQU7p>4*&z}raGg!`LsaV6-gr>77&ojxz{7_V>x8(jHVpD}22#v^2bD`et zsTKyOvVX?&G}sJn{0smNxv6r3xuMT|SwbW_I zu3OSJt?2U{z3c{lUeUB_1%>!nvBvme&pODm{b^Dg}z?~Ccrd+~ud~bpz=|eY= zios{sMmLrc1a0(vTbGYh@7U{K5Lo|fLH)B5_xSJ|HVMzPF8 zP7|N&(42q4GnB|PgcdF8yNHby+j~p1RHy@T!HGE!Fioh+{!uIqxWY8i%0zNe*Pnuj z$2sPTWGTgJ+B&F~EXREFG&&i#T*jq>>BcWS8#(IMsndCNDNt zmyCR>l7v=%C&}#|Y7goP!)K8m8aJ}Q!^p!bS^vtBY{#s;%xP}Mbz^=Ge83(jPBKq1 z-VXlswb1M}902a${Z8<{5=&v{a&I{L1o~_ne1di!uF& z0mH*Rd+ZwlqsO0LS=`$g)|n^3&gG2#wzSe5(5iwn9rBbXrWz(bKg98RVWwgb2zhx$ zl~h?8G^t@=!7h1Bw7~>P&q(-gZdfQDVZ+9|BcCgG=3{S!U*3w}(rQ?Vy0{VOsyc`f zoTP!{;yC`QEdJQ-Nsw@!M+OEhq<{S4&(Zq#k3M$9H;R-?H~W5~>i=WK@~Jru9eH1G zbiq#mtv^07Q5PxRKG(GzUI_jKRj7+UwmkQ6eeU9Pj>pm#32oR`{R*x@J+Q~yAlgK3~U-VkZ;_=ZF;FMQb(R>CNMZJGQNEMGq;-id1=1PkHaA1=}+ zxOA&z@$a(w`asg8bEDRu$Rvg(tX!v1K6UtcNW<+2d1$YkgGYqe5}_TH*2eO85CN#F z02+dn*%OYVK=BVi*EJ7wV6?y3N+%utcD15%Z(WDiZcG~N3x#ji06q3Nqxa9C)|BS*^5_Pu|sjZ+^BMidJeoOO9A^rxJr6OjBcFw>ni2;+PJ+Dq!| zlX9acsB7)@H#J_}7XPzA`{RCI>WBMz#cr=lYv_ZqoJ9`*%RKD5wlg?8`i}cU^;r)5 zX&DsvC16G=1r|ZG&Q)9?XP|i%1)A+Li_+P^2ZS@as^~31&tuwtK)2ZH%li1YU!T^| zJL}eLGqb0wW`uKNK=hUi>H=#}_R7Q_0R3J98#$96ZR&F!jI|@ORNtc3>hb~PQ4w$DX@uss% z#I$ghO>^$HXISd(TmDzCJ)ojG3X zfhpjq(3>-X6PTd~@qG4AY%8H;El_5GFa`&pL#6U}gE%W~!MsoADYYfXGe6nUGCXWB zJ+v^>*IVo=8IgQNN5VyzTI6JOa;Q*03TW?9iE8Jf$Iy;hgIe7TbB~X<%X)4Dd?AF6 zpS_fIV2eD5Q(sdlP7t{%G>KD;W;m1pBXSMzSLrX}Y-G610{89JX(uCjlN3y(N+ig0 z`@Z;t-n~9Y^L-6HsAR06cU3_BGV?HG*kEngw)PaVBJ-dEz3{c^e?gKIK>a&uNa_qB zCjAk70t>qb7=_$f4g9X^EHi!Bm?=9ipj5ih^Dp_{ad}lh-{2&_H$ie^D+9GUv5!RO zKz(dkl>3f9@p~S27(*@$#mDRgmobXoEDcU)AUJ-DOPb%(u`d0L-JZ#^ULLFGX}J7) zM=vttyS~cH>G+ehgbroS3-hFRM!X+BEo+qmKpeHt#U2^x8-S=F2pY}NA?T2UMfb}697Pb<~E<39}>qptIa4^fnM0^U)m`J70ux`X}iv~(XV z&W`*{ZnK(=t36r#>|S8MI5(F#3|eAa*I2!mvE6SoNacc^lqR$J(}_-=AqR_#;~vQs zQ+_7;-pK%^OWz^6rDzN!wxfEZ0)NYi5Lst@rG(!q%>hNfmxAL;Of>T{P?$%v(y?N^cdf*k)lyf+U>`v zc8emb_+uUSY9ty-a@+xURdBf^8}m(iRd<2O|dnQa`T%_ww|O*eZw_E{MD%9gbz%qS~~O`;ROUJf|_ zE#iBPaCGGbFFiT)IuJ&iv`$!I2A?o+uI_H~m>UqXI<%0SOiv#K5ZsQW?7B{I6H8d4 zS>!}(f{l-_ZAdVvi#sLR)Ac~hSe%%uR^w%NMd{a87llh1)PL{6f4pj74|9!=v(r!{ z9H-F){hS&iTPVzWfc`La1$ZS4YJ#^lWh&SN`!u`dHvrm+P;WBt*_44Gzc$lte{0f; z;NCSy3*7x#jexYFW%ar|Nk*vTh3A2*`hies2L+}=uuENxo3$>VJMFHxK8|u8dOAv4 zZ-f(nqy^T@3!^1Kln&-+*DP|MG_VCqo0eVlZ#)mSPQdV8oq7Fer8D4T_P6hK0y~y|m zVyjcW$2}!r)tj3IXw)oWbVOp8FCO%wJPqFggjp|NdCTB)KVjuOx1ae>q^@-@{7F2i zet^9EEJn%xV^*@Vjm-{ewF6r_msr2XN<$X@&4G*zeprnrI4Ty!+|Fs?GH>&|4maDA zCCD<129#`qRU*r#@soRIi?&_1f2S_ulD!Xq!@JqWLIFN;;*3s&fmCftin z`WPvBPsxrG865L!d_S=PBSCFiUB%yp+~$g+>2p$eU%%%5Y?CG4xMZ`)gcGW`&v#b! zg4>UqJ16o^c=f&r_z-b_b_4Z3gTJ=>sJ5kq{m#xZ#AIA`$PE}i-CMD~-X=$S6OrxIu8;BObdX$P~7 z>Av51?{p{8#kB#mG2b-p?cS<=V=wKzl)*C6?G?UDg>m8MkcT)>=&SwnhZyKdwHe~j ztl|mKC*%Fp@a#P{^=#?lJJYwYgY@yfpF>5I9z14DUK4$}*aGvKzI#a}-XF6xT%hJv z@_3+kP8bWvr=CoZ&75sSBA4v@0|fGmnrP+8B4^k=tMnueRyUrc&2_yvZP1geXF)&f znG%|JdvIq^Ad2h3$BRsZt(%h!ulEYAGMzRrB?;F#%&y~DY-#`owvxd`3XB5EThvaE z_(7pmedD%RO*DA%U{k7!FM*Hz%PL>1C{;S;q3KJ0M9*=&Mw&4b7NjB^FMFcHzFS?lk^{y?#ngqKaCo1i55i(?zU1C5v zF%SFLT~*D|=9ZAJ*I;341Uv_Y#{oqqrV8+KKKW#BunFBh55XfAfN6HH)P?spblfMr z<dJ1NLzsbh__+uedSPo8KE(X!e+B0hXO6E=KKi(y?kkA{~?IPcNwkA>Zqf z&r!z9z_S<6p(%)DOR{)7zstEZRe0e$L4`oPMNyc?pCRBdHNx-o!xR;$9o4vvhz&an z1RCuyy;Ki`SR!9SrS%w)B~neQNBc1Lr2DbMJ%UW*4>}t$0yD zP2Oe|k_82V1(kCBP2ydCi!yPoVzcoM9O_bxiWO_^1LSf3+Wj}=zv9)8ZRIy~ZYrA1 zDbbXXsk*mSd!}q906wb25n(GLTh1!%YnL)Le3kHAP4e50?3m_TzYmKXn6AUd;t=x0@NYpl8?VVlI$k=@STc?zLP#428hiwVy~NUJm?vVP^=oLZM? zUZS`RgN&?2fG;Dz*SIvuYU;h%nn$WUt@mFsNb!%49H(V;4I#VX-@9_x4e#@2oQYy+ z%%K+aQY@apo=~&Yfu*{%-d9>BlW!U5_!%zwMud++x5&!tB!x(_cgBs+C~ZjCkR-rn zQ62k5wlAA^+oUyqE}~FV6rYd;Y1ekUL}%^@&Sn>67pSScy^rGW#_ldRI>oW7W%fO# zPH#&m!V)7|H6j@ud%db~maJr51~M*RvAWJJw)`6qwB^-5U)ZT&FGcUDx>(*;g{Y2l zU!}!7Xm{M`?M&E#_0tknzwb;f459o~<@*P_lfr`yk!*vY@|WA~EOrgjS=nOK%Eu#& zX&olQcHPWUCU!mT;Ke4LI$d+Cba>6()IgrM7^G3L`%?3+LLi5K(kcb{66}dg3-=Sq z2q~!ZJ8x%>)Ikp#7q1vhiIkj>V6$@lV^}slkfU}D=p>PskT~5q2Fu=wbdjwO*=>DgRSuP#_8vgM(SaP-=&y;#$|c$Wz8^K#szcL?jD zd3z}cPheMwyU9i?J2cbodLrq4tXVc+|93gSfApZf$$o7x+x03_K`IiYTm+3))e8s8 z3WSc&GVxV+2BEL4KG!x=(G@!f)%aR04$Xg-##@QVs${Ip(=jSbWffT96Bm2>^C+l+ z1NXB4!E|uii?4|vy^sVR1)Jb3d;wkPb}=>vXoWB7w1i;}327=^+aoT#;}4 zr_q_L8T9;4os8T`+d@=_S0u>aaM5aP)4HfxAO{k&k_r+_~-VDwA4R-Nw2Np67I(MHrJ8q&~({6bPIj|Rn`X1Z=$}t`1E`a#J z<;qaavw3cE4D{}UG1k}P2uf0T-V5&F2~K|HO_ls7E<`SkH0!Pkmj5L)SD7TtvXq%q zfa$-8dRzgd=&(0rJM{T~Vcmg8uhsDcIaDZb;2+b&UMJ5D~V*&;P=R6E_M0?bHeNtyyEm*eEK_J{}Wz< zye}U9#A5XNK}Zp}a$p;)dJsAQyNUC3YIGUB^MDY(6sR=~MSWymUn@m9^|Ki72<@$^ z^d?J@AsVr(gSp@%%7rP>Lp;y>CB?H!@x90XYNw(#kTOV9eiNNLA_6PG;B#92GxoyB zT(Aa7q&2t)ZsIP2gD=-+xH4DN@7T@8)s8#JQ2SGIJTgQ`R7Xg#^PqBfLf~$saVUo-_OUM|h=QdW%7R={f7q0a9@_xX2GfmQ zaFb7Dx_yTqcvT1~V56Vvsy}Z4jl$ht0N;3lW(Ihr?sHyleiafx2_hY{Ih7k@VAye# zI{(x^SVXhR6B{ewoEr;Ks9=nWRY{zK$(ENt8J_dYQiZYHt(Y}ahmMX^ITarL1EF1r zqhP^2VmyQmUQ)UM6MDRzZL$*jkV=6c`L&ehd=Racb9)V(KMsn5Oa91GtP9Q~%Hqae zU+B1T?*QtW&MlkCeDef}MnOSpVo&uRyd4P}sOPro?b0T3EaD}e6(M32AUaaXo2B)4MPdsoPL&@tK#giQm`rcKbQ^|b;D!9S?N z3g2u8i;P|qHc2Gf_k>B@(_eB_5_3RZlna0w&3fF2_8ajSOJ+$7X>c2{@MQa4D(%CD z9Oct}LiQ8lTFe1fy;FYSvS2rw3%1DV#icDt&@UsK3}>AUR;~R~ zm=;|M5|WxAzXeDxaE9zx^qzjXlU4pM*lC;)G<~uBx) z?Ak<$O5v6cA=tq@WK$4F%?9C-md%6o;$#%BMNiXEPXh4y<&Y@oJ09a|zO_7Fy%4x7 zk$kG_QRa>;gsk8x&^=m$FY3=Y3A}x(Ye>i4VclVk@r;B?_Q5R8H@L0ym-z(<&1gAv z->fQ%^86%jBa97-^D$YT6Kk3GgesXYC1&Ws!a`FZOJC1B^enmag|}_J!_H!5s)xpv zk?=tNn(omoQX6@f6hX3>Mg!3!jt+T1|LM6eeU_PA?B$Z_&w~j+j|f~cqE>f+RB>`; z5Y}~Qp_>k4rFi;mCvaO2^?cNwDyF!O2@Cd!*E#lg-RHsk@C#$!`ebs*hMx0ax2JhZ z(DLXrVvXEW!)f=0xPgtq*Y7+V*`q#qj_z$D1t&B|wuq&7JW@%_X3$`1 zXU8m=j>q!MfQq@nwT;MS^|2589Tm@pfNpDA+s1Uz2G4h_+R9es3=k_HIJYLyCLG$o zl$H>MdCzV>Ws!}2_aSAKL3!s_Gwdl>Y1{>#7AZXr`t^+;<2OlvJbut>4d6ZGR zs_4>ZlHFYmV?3e|web+VIhd`jTF1;z^_`4#{QD}a%%&qjcDGGQE*v~Tf*`H4B2FX% z*xu8gOHd9_-pv!($Xv`?_amW)AH6AuA8xEkjjJTz{Wi=o1pBg_Wu-6A^18&)=PC+( zjK{AcdJ=y6z3srwj3ck-E#zk%5oE4ke2}f7^_@r!X#S0S2vF#GPRIP;XxTeGQYgqs zET=EDtX4KS?k!oZSGf#Th(GmYRhkHroI~QQt)W&bmagwYN;ll9^HTNgDzd{j$6JI4 zICi6Z?|*enagB8V9zp@YhRqOHtZ{COy8K=T#%6fLN_AXGscs42pb6{J@zb+L4+h^_ zC(N5MbNWsUCIC6PmvJ}7?%vjgE@Isdm*?orcHBA;V{&&=!UI{u5?huo+;#0U^@pje zMK1dCMPBJd%%!u1L()}{h#WT&1`S~8*{iB&UJ;TIoV+MBz+bXf{1kGBL+o639+cV{ zqZOI?Eto+9PsyyLRrB*QCn{ z3+i5Hd0grN?PgNq`+fKJGDr~R3szc|>D%KMI#dxJ@v$TcW4Ig}bG9dyZ+^1rE!7HI zVF_Z~aSnGg6_QCKL^N!)k=$X)E2}fzZ0`#84I`59K=53h2!&HF;a?@LOsu`%tPJEjpz-^zQnbtOu!qD9lW>D43G{6drbG z@h;Fxn&9|yWy6y_L7R+Rqepq2=^r*n>Y%GJ9t2Su!@RE7lNX?sqSZll~`xe&tf@i0orw!JuKS|<*PPS3bOxzFHbvn;Y z*9R7>c_6`Wn3qR^Jnjb5+J zZLa2D$w&sEp5eJ7irZOHHz||k|HRajqn;B}zF2c2he{p?yG48kDb-s?B*W;OuqP}8 z$9c6UV%AOfcekny9eL7N0hE<3Dig~-q4+e?+Am^5WTS*%eUI7CVWWA?BXVxrWvB!D z;dv2ie!+G=0@CJMQI0)YPT{hjkSHA@rfe^Juwbo@@ERQI&yZ07#BSP?UmQ=Jz4d9r zDRkj{FEuN`P#R6?_y@ch#8}JvT33=Jdv+sAQflO1RL-nPr+#Hmr8*tlBq>TGF@!`rh&+ zYIcWJV{N5ki%s)<0#qj?l61_P6H=_xKO#l+>L((ZB*#GkkgvG^d#c>uHh$#Qm>86+ z9IBn!DkyP?k&w5rND$%aj_s|ytwz5Bvs<0PbJ!Gi%z4nFVF(fcBT?*=?`&o^sP!kN zLKGKP{cj;_xW7xB_}qa_HKf7~0Q79A!pJf#Rju(TynT&VOHpdR+v&^s~W??+K+A(PNd+5g;`6x z_g0*Lap6k)7TbnD=hwehzNEU|V5FV@kg4(~w1*iy zQfVqy+Ssj@CWjeyJ42dgBCn>Kbyo++0Rk?>Y}3Sjao9+DWKqrUNRE)nrO{u>GtM1ZPlzy zFb1xy06{{MR;3dE1|;C03BJ~}x^x}vv9+)@Y5h~pPfmX{oLy>6ds^Nr5}4BdNG_&* zAo5Ic%rC;G*NY$19W2>6X5C{`!!vL!Mik!ak7fAiBQ!*9`Yp7t$giZlB3$}hP(np6F&!gPp;6+5Vej`wK*^I=3RfX{lqZ2S<#!$F=V821;^*Bc4Ok z;DI+^pEu{BLIBP-K=fqzHg5CUSXakhNDVso^adiyVexcFhudA{GVAz+Z2gl-7{f@k z=oVST=IyVh_`!vn;DMY!eO>dR3Y}TPW>Pzq*H)v!b>&<;=5%a*VA_iEE#q;$Ht83e zNK&JP9JKIp6QQBzS{2WpiYX8TPbe97BMClww$+5y!!#PYkI)$k4h9p=v6Fy*JhAB` zgQ#N8;<&TQ(+|78;K44Bl)9y$>mZzbH`0fswYf*I8G;GHX~6+dvolujrVY6G?`_DI zn0^JJ?86{;@0{x!YPqvyn6I$;tx$g6aseiBc8>oH@}UC+b);j=r+yCC0qr-Q3@*OKhru6=)?M%tAbW=x{ksv zk2kXCCsL5bC>m6g1e=-;Rk-ZVzuHEbJ4neebyRw|m9Go26ohlk2JhMu50j0K(=3%s zGNvc1y4-+((K#gFGUE0RWb_rIuqSzV}eynkjrh z^dvJW!z2wt|EbBP)mMvmxqEe~nq=~!tZ=N*x0SK=i(M zVhI%~VLn>GA5#1BMeS_zvIlq_=Rq%UKdY3^G3jBjuXGt{S*0Bb?I0O6+m6e@hRU`w zoyE*d((cn=3(zp`VhHIA=b@)8azDQ<1kmZjQqL@Av~N6buu+@mjKdlq!!2x{o!)_$ zPTZzJJVyMrp>~ookF4FrgN20n6%a6Kg}gOf9??BiAwArI1D5*sC$#v!Avq7IFRpGn z^mCxKbZ&6HJt$;s{4SEtIB_~SB0JtG}Vtl>B+Pj7}XM;2TP&Ec}H4FH2+wY}!yvGnJAk`OZ% ziN?$Sau!q=6SU?ozLWWf7q36XMdG5caZ~3~TK!W<^XJz%E@vvHs5y#j5(?jz2d&mjla+OPD3z0L?{zAh3?Uj!poE-3lN-*Fj ze8C@Yyo3ZqCfP=sGW_!a{(To*5Nd(x8R_q7UVmS}zq}HmhRYQnWoHmDW%pmQ1QKOc07zh39yT)YT3B-@^UH{LQ`@aG6_oMsQSN|I@e~qGlUz7ef zVE%QO{C$)DH(>t07ytI^e*@-!1Lp4~dq;jo{!Uo~C?uR#KeA3hIY&Kn5^Ki7Ae7-H?cfcqCpR zD1=D*$b$m?DV;g87zL&LZso^#9vq=;Snc)auH4`Z2 zO)@`OBS<0}vcyTnksN-LtTBF4I&v zmv1J^p$H+8`rpvU6U6-u54rqC_%d`xM%I7KNu!vr06L z-puhYaEwpA8oqo1G!EUyLT?% z^L)b=@FkOc!D=MB@-63^v`9}6$}@1k;q@un?NS4mS#M`E{g<=`?EQc>WrejkB>xc8 zngCv3)lZCuPyD?{7rb%UsUE-dO0jru>Zk+f8Nel9Dfzg=h7V4d>;bv8fE&njQv;3V zNk?6+X?}E}ebg(r8`uRAvSz6D!rM2bimN9mQHFLD8T2nR=lm@jK^UJNJET8h^JQcP z(d$0N_hJ3;V3rup-$w%N!RH5z5+E}{gyJ{+695HSCg7d?%s;@5___@s$bMx1@JJIu zrQzuZWE3B9!6$)@(#-I)zGjFY_t0Ksy!r77D+@JIXbcTc9nn?zjXdY`2d%;(pU`3i z&j%>KhfSB#kHN2-kdID%z@96|wxzA(~4?d!JQ}?`x z%Zo3Mu^Xlrq$;uapH(zrFW~LL(Ff=>#UcuJAU+rrQ$@tv7ZLiTN)C@sVi8T6147FPDPU!WZ=4>y7D4F^?t~B)or%y~=Kk#_D_iF3fjWJ@Ll(CY*~RYOAOEgrtsO@e!5eGcma(xe^*KA`J1` zAH9i9!S}nI18E?dYm25n&9BIbH?@-Zl(MnUB?HN^-;vWKmij75nv-f#@=-!aVmrAF z#qDs^1hwS~$kT{*C{SYr`QgPp-TjA>LlB;Hat->xb_3XOt|ioTge@4t#I zls54NNQx$9#Onv^C+Uai`z^k1RiLPi@r|j#kjoWZFB+oACczaN&UPKDER`wco-&$Z zCJ^Vyx*U#OZ#^PfgB&3lQ5b2gP_N*x;I5Q(y?fNetb4}#?qzm(YPtw%g*ym35yeAJJ($~L3VM^k=S{g4}BHm5Y#GoLiS9A(Ul=+EhQ zPVG<8VY^+v3fd^D4solzFh6eI;M<7BO~S3ieTl1#3ubG3{W8fqsWWLM3C7l5YO0Q0 zhM>Np4lC1Uj!)d_Q8K0|3FG>jU$C9SFvy=ZlgF2D@V!LUBvZDe4^u0KQG?@6g=U4K zvyhxLlPZ%=p~CK0eucFBw5)pZdJXTch5_O!-LN;wjo-h9ey!I_)yrHkAo~%WH%ObQ zp2^e3X;5w(y3fC)L0J@%A{iqY(J7-iF*2*9TisvUk7BWEO39Y@x~4s&2&$&`Q*11q ztC_dMBhxkcoDP{UOf@V8YX(b)ew$vAVUwOnjYjREs#WPm(M9uAOOl0_foFT5`I13N z-?y37viw|zIj8zRyd;inof6ipz+)i-JX^!K(CXCCg&r>fyxS_^^oZUbvGx+2x)N%}56=Z7n`o z+F@kX{9Lj%WmmS6+udLANvBcGmX;8Yh+q!wHUzUNcjaxtkxrHK3c`47P0(cwtc?y9 zv9EPzbQO&N_AmAy6alwrg&0T{NWPF<0?EB8n|CbMIq+T1jr$arNyC~unA>aSUwT$L zEJUFfT12PC%0~rZYG%u17Ow6x8{JXH%n%%meoJkyW?YjHdGq#IBAh&#!cPsk#JqUc zMBFH?OQ8pS^=LV1*}9vc^RV;M>$UM~XGfDdr<=g5#@9O*&+-Gt3N)vyWSzAds~3*z zHlwVFtt2M*y2ZL9jPT~HLuW@6(iMv8bWKZkSKEZDgcKGa41F5UEL$yKSZ>c4m*ksI zgGsdqkM+A3Ll@;JpfQ_sYMv_RGd(B%+mP&&>`Ui)Cq!rH%zE{qYNEcH_T)gpyjK27 z&q2mPMUBc<-FrO^TU}JMD8s1ISKo^vQibN?qd6N{8@{+vNvn_z_loV5srKpd`Z+7_ zVfcs#t&jN$pX1khcbv*wC0tSKPTaTmglf6*?QTo6=eNG$@S-@{K&e8p0Eq8OQzYAYR{F(+Z4P9VUI3$80*|`wV{0Lp0n43 zk8@GA1$x{#uT8HFww5xep2*Rr{t(E&9z7d3&V?LprH||Omjs(c1y%|(-Nv66$~8ai z&+VrZD;3)i&lm3xKM$XA`*qa1*BuTXUJ9tExhX%5AIwYha=wI^=y$g4SqyF_3P{a< zuj9HHJF$Wpt}PEco9%wf37yTW6FBj`{(5O})E>RQC3hwlpB5qD?16N3;6iC-ygGO4 ze|Y(dTg?loDUm)%sM*88A!FYCgNIA{f&&L92q!7>M#%+!E9Ggeva;_9M`-I?HWo4) z1*X1vf6yDDkp5YV!l+wy=X{gGLIP17L_s(Z znhzWTF&zB=f&U76BcZGnKIWa{23_vd>}K(~pM;xhA)aYU>Nfbmqw-yDm2fC)GcDS5 zskqz67msSs8a;_9R{jRH_Ezs!s+*Fl;39cu8Od=rWC_AjKR6V-#(X_(akUxXGa)XD zeH2%y{p3xUwPLwC0+tO~7gj)eJVKXzoqA{Cs2AC)hD>GDO^x`h(9OIbYUDG%qXGVl zCW8pH%G;aE%*R9?%(Ts>h%0%~F@xR}arrl_EkPRy)Ec8zAIi88x|HgYgnzQu5wG$z zLw4jDmZ{`YH7nflPojKUJrYq?@;s|#Ce4X9ivvU^X*D7%I3PxJI@{UPUj|Oh0CPeA z17>M5oPOO*3li$zNXwA=B-mg`)cXE~yt(1OdulEww=Jeb(Mn2Rg%qNZD5V*_JAqV^K{0!@KV!9kPm^-W3SQl@??AF zOEvFy{q4;l{9}By>-Dr7vkLbsA3;{ct1T0OFt0s7q84oeyAF~;I_>??q**T*&s@mO zUXVbGW6EZY*jwtkb^>S3sHp9!GP;Y|(>iR(&2eFr_VEuDQ8~V2r6Zl&V;#>YCG9?y zlUSgyy$;vMD&EH`ubZ!SZ*O)x#=cd&dbciVl$cwacH=p&<=iUJ9)X8Vq&245*0fuy z(MO^S5EJy-ubZj9*7(QGvfK^%#r$Id90J$n=+`qJ9)QXapFCy%#$0_AV zxz#jQJM4>nUr6hfjn`rFnX^vQPxKdz714s!8U+>Bwfk*J$K$WZH8E$rZ?A28+~~;S z2Mg(0<4=d{2_j|Cc6F{dR5H_UuV7mB!`56uK0ufut3SJ0WV*@F@H;&kmTTvGLihe> z@C|@5je6w=JvRN8;E&%@4a<@m5=~MkWvHtvZUk-*1q?Q48Rpsr(ik((nnoFf;7SN+;zZ0j0Lg;<^nUQR~> zT(}e^z+b(V5w>W?K6kasv3c~+1kE(uXOl%vWzQkOyp1U?HE`H`iN=Ts;6SDyDu3nZ zn+5M=M1Pv^dZE%rMRj%k{Fe@!E#Do6f6ncod=<`8W9wc(Sir!@oSwyBo}3>`|ei( zfu&Rgj?zY`^PK0t1InrPWSZ_)R6ZT8+_|mXREqQL%j=Vh`iN`1*XBwNLy}ajr%kN< zQ}xrQV>k7;*Sk?cjedBM`DF7X=KV%D z3#i@`FM@+UYt_vj7dVUNz0VIN1jx3Q?)PlN{5$Wh!%A0tbdLDMS!#}gKi43m#tu3U zkYZ}C91^dZu1O${&D&2qxaEmgx(-4|!irAW3toLuc^19H*U(KJu2 zt5!ZwL=^IPZ?nghm1=;Izo#x$b>D?(S(V%hK-mqt#C=lFV^&;M*J80|@z^bcA0FrMALptLSVAjV zuzCwj`A>dDwJO?qU2p2NN2Erj>a1-Ryyb;%Np-$Zn^JvSb8yIFwbl1DKcE~>y5FWu z)!vWWbRb>ztT%xrQzPI!zR-iBdansNlR=~0q3ZB;fA2|2BPZ|a6OEDSd^#hn%SYV# z0_yV#m+0d7gD(<9qYqnM?Kbl(#8CN;HppStALlHoTfQvSb?#7|#2LSmcVcE^G_NMUY(6TA z=wcvf@zvVFUY^c1ZNcGcf7;v%dE91K>%7)8hs9T0#akcnQ=7GFoUAk%h0kScV6}$` zP?{B|en-hE-jfLfTgVFbnbZ<{$8l>6h4do(pi$e(B5T_DTg_eN>eDM{XT-|XCyuGz z8fybZ?%;8ABi^$YQ^uD|+(UZRRyR->KX!Jv*@ymLX9#PFOs0eyrmBM3y;0s*OKVgD z=aNUMr?n=9FD?Ozxy5ACJ{tJtUVb`lQ?II8Gsty)G`vK^-N?j*R(*4^xfCE2OC=ZS zzC6P-|Ag|$Sd~*vBFDW0YHyCdOz4P*Onq9^pUlDJLJ(%FB45w}-CTibou985^nKyd zT7-^YPxIkK+A}{JKQx=E*?cq}Px0*>0Xj{njj~;X_ngB=J5O&=$&9|OuyQffB_;5% zwLYc4IS4h15ppzC5RC(#KI|6|s9uhd&S3Ifm$vQy{IKrY>vDRI(0y~ZkeO*}g}BG2 z3=b?2##O&n#p|^nrn9xNZ+D)|?P%_B*vrw*cY4yZFp=VZF=JtF;9O$TA9?Ey-ArN> z_VRT+KfM)qbD;D3Bw$4{KPz5%ZaWjV^WyY5Uf8cLY!@&G@)vOZ)ykj|SZgdkjnNMX zLu>kK&9R|Ok3z5Gyv#c^wY&gs#qn%8h)d)n)V!^-R4ZaoElSTr;bvrx}qVamKKC%EpQPeg9!|u-vVY5fyI2>Ckpi8Qj zpramaHt!jY`P7!uGTqZJ0xFbP^zm)>8#8Ft@OG^$p&by(x>WX??advtGTYXLHabfJ z*sL0=Xf<~8J|bTNmWFl$7OkX;OQdx5_$bJ0eJK0ar;52Z%wF>kh}O(iJ!f8IXN>P^Wt90%#F4)KBeq{qodv7S{&eo#w( zWh7O>-3UqSlN(pq953Ac{K@+)$b0SmT;-Rk%ARopO-2SNU5;GkmWBsckHchsyBxGM z)o1e=xR802(tqvM@@8EcyS2C$xGMEvq)rzascw3y%Ei6S&WPw*b6R519vP9-t(4q^ zdG(fPJEz-5z2ou*5Sbm$NU!~txG*@r!-u^#Drh_VZ}CuA`ssLUxFs@!)X+jfYMa8n*O`|i-cyNbRSDY0G!vmUv61=G_$HT~vZO<>-U zhW4Oer(E(9U1uQePUZG=5nSW>c4mrpeSqL^lVe1_E?d zt+s}&0W`ZEwuxOouaYEA)Pb2*ja<@VdPn#W2wX5Ilw<|&o z%$G9h=#ecrir#!UuWo&hrWhF(L^+dQ^{%w&^ypF8C0a!h^Vm~|Y{#XI52@}uRcH*6 z1f=t`mdHlCpA84MTMNuE4|p|^Kej#9&X$yO-s1zm408#y22(`sxX?!6@o|)%bTfr| zeRV(uN9Yjp#%(L$Uc%ZYnQ(VKcin3OJB9Hz$LP90RX>CpF!Lbx(mvH{8rOowd0|BBg&8b53@uAaU7|C0mJbI0r?T2L!=Gh_fOJ7pY z?UwB;l+;HwaH?2-9?KEYVsnU%hPOt_m=e$UEt~`Sc&j3yI>LP#verF*6+V+4;ofYi zwJ;e$CyPqrR!}1T3?H@qt6dz)_G8#2$-HYP`}(6C^{tN*POeC>MlG~SZ8WQ;hh+?|t^AvLYpagC1Kj26qnXf(U z&ftNAGFDx?9VX8~N0H(qmHD;}KVYU_t}uDOl&y)}i7VRwJOoLcZ)5V;<_PrlK^kDy zDYny%UJ>=%Y5i$AF+>f8a|<2-{jeiU^PWbP<3fyhkHdHZ!atP?*rsM!oypq}^}{m! zs%PI{qp}yo*SQ_#t`2z+qJt*XUrR?XuMqn68xwR%9{@Arbj##`Ww9c&ch;L{K+%eEmty$%9kv+6BY&1qD$|&vx zE4E(xP<#EO0-RUgtG5*_KtT0jL_xgOF`)HVs|v$X>rjD>=6F_XS@+M=lv8OzBYtzLv2Ks4wEIeDN9;5S53O!TRMD{rM>pBzHRelg7O@U zi-%1{sEUlBw14&H*x%36m+cc zhu-wzyb&Z?`)W#66^-%nL9~9h&1>feX*WkXOS3LD_I|J@oe>rjd`%y_g+s6oXT|z< zpu($C0}C$RI&UwLTs(zeFV!CQ2}2;%+_y6mlhtqRn%A$KXRAqmKr?!UW`=1ko7N)x z0gAJ3nlzQZ)H#im1}-BsYBaRc_Ozh+xn7JTNoqX zwuWK1*H~9E*e7p2CEIwtgJO%fD~nk8z-|UsKDimfd$C4f&%8p)oUYl~D`!rT0NcpF zAuUWl)!vCnP;ai-X*{W$jpv(8&6qz=H$zW-*8d{bUCz@XTTf3pU!eriuFwZhXj<2! z_msYpwz=G;V$Nx)t7rL}XWdkNi<>brEMc7GM=xbY{|9Q&JX zKT|n8k$;$f?2XrZ{dMK6(swIYg`s7V3Eo=XeS^Y`ZKlyC@)&cChAHoS@oJk!hkl;H z+R~jh{%PXO>1uuR4$T$Xx9fsc?@ApSlxD}sZ4&uB>tDo$Y*<~R2eqlip9^x_Xu*ZYvK zijcsn^Ye=Q^8bQR$Vb7Rx<&OtuXlCav6*rCeIJ;FzsKdjTDF4?sRka5 za#dfWxC#oS!S=B*CQQ;izja>fF`#6`YuB3sj~Gm7eZW+{yg-|oN>F|B-Rric9q1Z& zIP${kkUp>_dBR1^ z{9(Q!QyIJ0DKUD6Mx!ic_*jRU%XK@h`;9D`D57Bc3W8F!%*NN4%^z1vER3eEhKT8C z2ui6~Rc~`iu0t@`s?kD7(Fw@Xt6{3~IXaX^=1%RTLJd`<=?D7gbEAQyoZf9KbSg+> z#dmS}16`g1JFE~lK83DXV2#U-@kis*ls1Br_P4CAq2U_{n2b>{-5K=YSm;bLKFvhf z)y~FZH);U{Qpg%yZWgM_byPY}LZ-V5XHgPniTI=(v5}3|${*B76a+vEzpfE)v`+UV z(Tu~YQ72fCH{V!Blxs@nC9TM8IRiSaz^#Hp;L!1qxv0O4*wkk{1O#}*QojZXWj8sc z+_TidG9Bry{&(cx&!(a=A0i4H8LJqny?Z(JGmB;W8{GMH7diQ``AO2!*0Z4Zfnjq2 zq|B--Mg<$`b1GxPH~H?<@#(x#U!RI+>K7u!h-!r89)4bJb_CB1tC@J$I^Tlx!yov= zJQG`^OC-O_HCgZ`$0pEK84)XCdDtwwUw34rdOm5A2TPR4G$6(YSni!lA$0BA@C-Z zh>RVFbayf$ogXgi%1=f2Yt=bKxgNypo;?NjNcE|Y8;{L>AMJ*^2B95FuuN`)g5t zJK=%jrG5^N?1bz}00}UweQKTHC9L)C`%i05maf|$VH$WR!H@5s%B8D-cDNEV+FX+H zR$2;16f@mCF&<|Vr@zwk-7Q|=8h|_v1Ajyc2}eRvNer&HUxu@LH*!scj5?Hi2ezrt;{&5*CSrLYVEikUWo zDo33)i#1!p{_1Sj4CJW=omzLnVj_1r)@%&)zyX-G5-;jzbU(5K}-`TH5F)wOqUOG-i+81#uw%Ts|x9oO0x(gJhrxD4M4F4Ua$L{5TZJ2&XuoinsZuYNtcnynVpE26*FTg;Y-sWD6yPF^K z>F_97S$l2dWBT5!W`wxi*qB%ItwkOV^v$lL8Q)!TwQCF7J{E}T&0C4x8Ibu>xb)5g zY=RA8hW*G9RgtMGyH+S_w+P26%59?!i!~(cM~u%6teryW zhGqs&!8L6zfE=)1$e1qV&I5ZO-)M6vnCfXQY`_|v@@<+v#?x1Uq(81-k7dj4pyrdB zW+F9{+sxXnBf<4zBn>+}Rs|N1wzjL{M6nQz9#(7)AW47j2oY zSetp^Dikwb6UU9!jKvft92!WQwj^5EclaUN;1^<1Wtr-^r1>3aM?pS8OD5U4-b`^v zq?72^`*9{El7g-^QlDFkCVNu9wMd=UaCR;!%j~1c6^r?_=Bst1FW!|}#AFD85+nU! zL>F3gkT$vduSIrpvVAgKV9Mx-X-imwQ_dw=vK)5Ec6CduO;+-pV{ym^L~p&#A;f!k zszCl3h>ZNvXUdNae3d*D8z$82^f3$)H*^9pE^kMuNF5frVyq#x&Jt=H1=C`yl%}g< z^WxaSDj!V{uz0{3w6TCkrdQOK!G!LWs?mQnT}ReC1e|y)Dk*->vN+@ny14z4bX8~_ z%Vfzj?Hrfr8q=De*!hI??)f`$Jp%pwXg6%eT3?}XNgX(r${`@!lTs*|>c&D(gpRCx z;Etbh1x;LD&$^=YOGx(Fm}rgz=fz$fQ77>zcBq6tzOP7+5`C~cWn*Uuaj^-&ogAyn zCoj!lL`sAO5(oBXFau$f3t_?YO6tz;(d+#n6CPOrJA!@?>0PCnK7mjx;gl*OJWi_i z^Z}E6j2d}zzsKoCFelR_m^786^G+>*4GJEocn_%2{P zlcpy$sXk-4yh3!i5z47=Hlzu+NXm>$oUml2N&J(9mH&p7dQBdTS>@vNl+2K&nm z+?VMCDS{DV>uWm&XYtCQJ^^H*Nvh}V3u+Nm`%_^)IgkKH`q6qlPlR{bd ziYSP{tI112!ASp1WsN(`&|aSRi%M}HE0bm0vX4*BD7kAn`{O~1(5cr_=Vckx4 zkvm*E7~oHZ7-SKG+&v%?PLc1LNhiaNa}P|)4?68(9CV=``y83)%ceAG?|BLhv5h#B zjG}PEG`h7x5(0N2_z~>`>SC9Oiz>DC@Oj+FtbEhXy;B5soorcov!3H zie~zQ7LQoV+qg4qUxw2v+>0+f;)u9l#PlFWFdinl{;$fx@eOxem2J{=UM}`dGOXru zWH*Bam#(&Q@~Cg@C)5QRG5{CyDZ_7nLu(7dAW?j$-vSAvdyzXx5QT%j1}s(k94v~} z?4;1y7INdvD0^V6#jYU8DYGx&;IUw@iv}s1D;T$dc=gak>0x3xDM^UE<-cL^!phxQ`TaRrMrU zwAiZ4N4=4ns7ZfL)K*n1Gh^sNLz6*{GM;}28|!2U1E<_X)2`m12W zm%^Odtn;VyhY zaz*!$eG&j+FvOCpmm>fHY|bA(oapiVGFy`W%%U#g8QQ>Y6EDm+67L0Xrt)_G1FVYj zG%gY$fFeGE@Q9Th2fQH(fYeb)jqic77~~|2K7}0mD8xh>YFu>JzFjf3!2;wFQj7HT zzQ=qbr5h}=3_|M4f%2?py3@wL=l?wX4F!8w&A5;(OmDh{7lCYtF2=5&O>=3&4)Bf@ zmp0~bR;9^3yYKyXbOVDeG{9$42YZGgrxs9?&}T%smSc?PHb2<(xx)QQ^bC<_-!uZ{3Xf- z-J@JbqmKXy4Z`~G5ry;BMsm4mM+r$y3F$cc;?~bVIJgdvzegXv^4KK>BT+qW0gr_JZA# zd&Xx&Y*g`ze?FWA&q!lce1;%=^7<3)HcJ&P`mDcd$hrCE63m|TcC&|lJzk|Nrw_Cu z9G{@dm*HfmsgYX)O&7687f$}@v3`429jR+;fqB08;+{u>>1GHqt(F!-nfZ^P$VgP| z3_daLar8pvI0s-oHI(VvgLyLp?SXCV?8)B4@+1BTtYPKVq#{R8=G-GCxI0C@6A*6r z*Aq*AoKm<4WwY@VoBMsjS1tCoO%9HPi^)Np>J7=ck+c12gIPBwN!S(DUv^}>eu!`M z#a6sU-4xL9_~)oxX#||Tp@ttf1n0AM-wo@kbj`I1j>leksy)SgnuN%eHKc`UKxS+J zK?wh!p#v9r!_EttJ~pIpK&_EnO9(aGm0n2^fenec6i_QSB`2%aSIQ|=K~sy4N|RQe ze9YDDT?x1O$7_)ox;pEt3l%QvCJW^7j<$!j3}Cs`i}+xu*L!SqiH0<^_d*-1d91ysU??n@YUEZmSF5mI^p!Ub7xEyR*>O1db?>w{%Zax``Ml6ZCcOWKAJ++^IP2y^7zZ z)<^KD3+@{dZrDzz-{ft3`!HHAS@^EE59SBX{X8~HDR+u0*kXf+3B!M%3h z)i63B4Bc4BW^jP2KHqpE;o6RFP_V+8Y5($0#Tf;N9;-kLr77t}9aKFT-T6M90vuhV zf=7;=#5i0REtX!U3Nyqk@><{TjueG3{=#8bo^J`T;#K60QN*g)>Gh}k?KGfsPH2#4 zI*Q{&4l_)PaY@NzPGl!RTr7U?6!o%an>|NPI;)SPGT$!`#PgUgY=|G-_Y^)9SPRHwE!*I;U!FwBo!#C zNs6OI{<+O3LV|qZOCME-2NmD4uXENBcWx@aiit)(tt{A#ef-c(0wuxi$ioX@fZ$3} z)}KYXM|5X0$b?ZzTrjr9qc}WR7jQLPIdlU zip5w*+T=WP=(O?Qm8(^#)^$cYd)DM9v@9M!_sT72F|xG?lEQDi&mVVga{!9n8a8x5 zmG8^exMKR4W3Yqsw3|5i+i-7Dkklcr8tl9YMZ9EE!NAs{BxI53w?@H%Qm8&z%HXu5 zV0XG%U;Fp9|2$@qS7(wHu`a|2WSf$>*U5>GF($#inSn z0S`eTR*-Ajv!4Yk*sLoaQ;uw7#=i)zF<|C+q1QZ7-YP-@zBG<24 zbqzHP2HP6q`UVss(v8oo36$U*N=<4qT_|fOKty$|jfqh- zMO|qR8w;zVPX05O9{}BN+h&j?<_SMz_~}J&W$hFkT+_V_*^};e%g&PLxuBN8G)yVa zkfG4GwWzz@4(9u_Xq!CU45eNA$Xd$(YwH5H7T$OtXDKs1Qv!6ObX_&8wMg_WHUQjN zl+XAd;GQS@VB^Tf&&x2yXniGzc}4#mQm!PlTp8xAPw&9eYw>N(Z!OS}xa+^}DPcuE zZ}ki&Ma~}`Eb8skNkr0h9LX%z{DYiNda9#X`OSH=Zh$H>AZ_nopkMu&E2nQE{OHw4 zWuSJ|BA=%4IphzJ5b`oQ&@X0d4~T2c=j!9l_+2gmDbfp3DU`8aAT1_stsZTd`L$@f zOm$LsYmxTcCq!2!H-ZzP4Lkey+Z6k}t^9>dAyx2lB7erx)GY8J9hnp#IVgpO!X!B^ z)cZbVVMn;p_0_&?_YON0Y_DxiYt471vueTq9kX!0i|8K;YyFqYi8mZIbCn{yq@@m_ zB@8?EJzDL&5%fUU%yf(20!)?eWz2U7?E_O@z)FG0h4Kv4`c5N7Ela4sGoJ%> zPJk_AOZEh#G!9eu8`~e{efoCK7G<9Qxh)?-8YGvTgn{X8Gve7iAb448I@n&Sb+pF4 zIQ>8+bc4h~kh*~J-pbY@)mgBM**xRXKfw!!ko2};Sb&^+oT_P9Xfn(&%Epel_ka7{F+cBft2fLK8H+?-*(tx@-e3fDj`RgEeaG=B)!zb#ecS_uGN;*mM zQje9FA|a|*;>)5bwYIboq-X*%qs9W1=~A+2sDE-3@qiehI(SZo`=!y_#lz3N+H)eQ z2@{jMGO*KaLq2&+g^QK4q?tc#T^v(4to*|`ID{@CL3>!cR91nEzd0|qCTxl4%gs-Q zSB@U+iP4ev+&zInP2C6g@vf!*9R!JF#9z4;gkA$xT!jBkf~@F>eFHcA2!%nDyZvd-+2b08#WxKFAa!uswg;T<#kxs4+;TKU_NKujD zs(^4{W=Apq=X$SyD#nvskrub;tyU@78tl$**v(Fc-X7WBwoSUqkBegg`+Q_S{pU7n z1Tl#YMwynM?(5FgFzpbFX;QTSY@oyP43#cUZCF3a7>~BvWtdiQTWzUXp&9?%{F#ora0|L2qyYz(hGk_mu zI}q#F!LpXfxel`S+>X#6M`dnAZ#zh8%W(lwsu6|c4ImOHn{j(bjN$4WqXVt<)+0`twz#WXd5yoGspPuMz(A&^wz1> z`9tht&*IqszLMa3f@YfQ6F+a=8uX?15i$LSRafvwZQe+$l`t9i;u!hAw&;>d9~+6$ zyGD4W^pPKEI+pysE?x*QEG7Or zs3YJ26m1^^Ai4%>$@tS-B$slht6o`!A{lsw8NL7c504o+lwAW& z^9Vtd?M!dp|Fi}mkR`?^Q+{qfFcg-mGiz#jeXJJtLGASWsTrOm9}m~QLC%$_L)6vh zf~Tf&k9Yp%IDq&7I|yS?k$ky=QhzGLBOYX9niQ6b6sD(gO0wt|I|v`dR~8MsK8W@r z89bDkDa(njoX7f)Qb5T-I!{AeLJ`>f@qNFR&d-jK8lZZ|ae7AJvAi`<>=l7EzD!G{yYD*7`M#XM#HvyQR~Bw|=iAjx?HFMnFBk6osw~46 zga-acDIY-&P*Us;6Qm6^lcAB}6A*zw&X0`%7TVgVNa zhUr~Jj0carr-m`F7ya|$C5G$k+NK(k>bhYeEhoEH$XU)+A{eSuh#arztO`|siaY)n z+lU7cK*Hp0FzIlCc*KJo`o0yL@teJB{?SthPxlKqjB?mgd%s)Mg_4IVH0fg_%^!`t z+g)90E_ArSVqeo;2E2YEs-mS6&%31UiHB9e-|t)hx2>j3zJ`OF zNh0Yr9JBVDR4%fRNIe$ID|A|>R2iu2;O2)EIon-NyI!V;bPk?|?uvq@f3K$If=2>U z>!&F9jpJ$1t&Y*Pd;VVKV}AM!$U7Kbtb1KQSVLOdOX6;l|Bo~a!jMqnB+^K139%Z~ zzuz*VZ|lfJ%8`qoF4~N`j*1qi$UR~CT>yw7qL{X@Sl_uMF51Wc{)McgN^f_uN)%jX z-G<~B*wbAz=J>5SM!WCScLw)g>U8{2;3m8~{cqD{eie)jZ0h?HO|Zv=f+`r8kA9K_ zVI71{L?ni5?vpvveylr7eEWn1&i4lW`)p)j+i|)R zyQ>x(FM{h$@QqR7W?j*4gYTdPDnFasuueUN+hEvZ$EwL9YpWxO`%Y-&P0jxPbphUQ zwg?&nu(Bq<7D2h=z9Y!l8>TU+oOb)x<51M^k?5|#B@}|M-otbQlRm9Zt=kJ#x!GO& z;jQ(OfO(N=sT=FSYgPI26_-B$)6n~|+e^v@wqz8OX9{=Cm3%}4N1dCeA&J)--Ym{c z?NO4sY;{3|_jvALV1AVI++?YKD!e|~cEsF&j(6J9f1cCr=;2tpKU+g~`MbLH z^#JG!W1S0r4OsFHL}yb+GIa>GA4J;arQp6Hz|4fd+#0sn^>7%W|Ya>Dy%^<+K&j`zsoz!PQI}3Pc5y03Ur58bXr` z!lHIPPL~-;#R=R!j)uTbL+XBA1)8><6YXN0H#R0stB944_H5qv<7FW8JF1mwXx=xx zzF&ZtfstnhQv$G9gLd@lukkp#xxMo7!b!ZVy`cu)FmEU>!Q~0k%uWQp;|GVH zxs-|RDQTUP;J#0V;b^(l*_*b(fcs`BeQ}C^aH^Ae~@VBf?Uewl#y7u z6pWm+)y;a%^tjmP_q(h`0IuK=g5E!)(*tQk&FyPXJ{KC*!mjEboQCDhUoD)5D>>r8t3T22wbew#!O z>PcjJ?-hd!sJ0Kn&e?3UWA;W2-Di<#D}8oLPu?$Mjz>ajSlwd*PsHz5yb=c9e(EuN zi2gqcX8`D9#C{V%*FU-P?l%E&6Mqtu-f#5J_om^%EwN}<{!guA3`jd`>6N6)! z!(r0=XV!IZD={lz`kzOcf3E7=23Ul*6kzf9YQsPv*|4g;5&7#Li3jdLH>mvpoX7ar zCf}aB#7#*TjQBSkxMaY4$l=%pf0t>WUIM27e*jDr%O0-#8;^@^2y`D48~o^zeGQJf zCItju2Vc}ZV=deJpcpE`Z=8}mfuV2*WeSB)!Y;kH5WxLnIo+VlNk>`;O{Fhs-G9kF1_x+#VRatn?+sO=XOW$jrZcUa5@9d23 zdsVrPvr#|z^S1!LeXc>#oZE-m_iLs2x}ZJ$&xOF0H?RSCr`CC*4tSql;)G_BkETWg z=6qJ6Emlr3o+cgXR3SYMqkB)Lj>+orS3d+W?FTvzH4DF_Q@*8oL%Y5f=`}wen|gay zUEvayqb3G)APs{w14wswcf(M_(0q^YIp=rI_s_s}!9362 zYp=cHUiV&Gw5fRPy?U4T`mCyJdwBcscm4aZGq>z7yT9E=9=Yb`JY6lO+Uk-2yU)Pb zG4`ZvO)}Gvwcq6v*VOE9sQEN^!d2m)_U2DO?l8*j`=9r^u~&Qgj|{jE=&@w)O}Go& zVZ^uj$4eFV1OHd6_#^v-?_aylJit0koo7aNJa44jhrRoURCzP8j$zITO(4(hj@+kP z$Lp9aBOQ?!$_^j&ZJF;_ZaN0KJu?)O_*%bw{$~OI*cv5o#J=T|V^c*ab+x+a4S&8M zBwh}4zUu4rePko_`pD%?LxtpEk1qqOs%fB-L3X&;&~HUV5(>YafwID@&K&(#0rXxr z1c9j6)FV#+*ttK(N%$B!2(l*NLVNM$cDIOgqa@EP0m(6C;+TbwC5cEosMvfT@BG!u z`8IB{p7h=Hfr_F5goN5n)pUuE3^-8Wzjd~wc^mrwneJs2_v}6Xxg&lPvNf5{@#&F zO}ad07Qi-bc(?vevUWaPc4;DTANj@q7aCje`d4z_6(o>js499M>LuTXfelF;qhYAK zsbRaJDRr43yk8%u+3-Ny8#W9FoW9e0T1*E!f#|B=2~%0s?6T_rh0Fml8X!kp{CX2M z{dR>PltYPd2wC$=bvv$X7BIma!h#jUTsq(h~^aQX*oa{91=iQoj7S zcd?mMc(<3Sq{RQ#?JO;%#an)TE&-NqqeV1b2)B1djt1KkNuAk#NT!$Qj*AVCmL1~e zitg=^F=Rx|iB4#7;34QfPLGOvkHoE5K;=k#HpEGGJ^CKp2CYRmuX)k%|3M~ZSOkm? zx;A!K;xJ}BdCD~wx*gfxkY~BeJOz7B{b@AYB?PqqTwFXTt0X>uOAe*h_6Ua4>_DEh``h{A$zSpVO`LB7O;P*2| zb3In@TYDRK#$!GK@aPMn@#x7^(B{=8mjJ^N#$WNnU)~mTHvB7GC6oF78Ty~CgenhUp{S!4Rz_dHij9q;%B)dIttT7zTutCffy4( z>lgIIext<<-RpW0NdqAaQoe8I=ocoEK=LR=ivM|@BLH+uq_R+Nt$duT92io_;9ksK z(a2+YeeRHukW>T~ab}z8GHb~K&WLm7RLHr%`cIspiS)c_M#D8e$a0sbPWTA=I-26l z*s@>Ij)&)0#;!KYb_O2g2}ub@NOB^qaq_U?n6r?V;c#Q2FTV0WBLItiPUu_>NWg?S zM;+m{eK%@FD)PG88sWM=N`7j`v&TSb!>tg$5tCPxxE8HKpc7YF%UM~XDCQu8N5Q82 zAu&A0yf3%qOY)r3)rbE?QM=3tP;?;?*Ti7+{_-ggJ+BTpwh!CpMN<54sEgxDGS4Lh zNp$``f#m{k@D1}=ba9O&-=8=f`fUSHxnxQ94`|i!|D(b%zCTBw`4ikbObIIQ| zaBHtS8dI{m=}4g|$kwKp;x}%rUQs&~!K)tX&j-fiUO%;4n3KWLAO1vVs6Dw*9-Hy= zD_?(WB1+G{_^gsU;!$c6MU^+G(|Fd=vAW%6FDV{oru(APo0v10j}B`JZ#vwiiX_J~ zptihG>6M#n=$QtgEQQGpV6&8sZu1pIH!n?p#DM}I&@ZEcJSij z%(Zm9Rn)bPhuReq6_1-NaxX_3{U0lMkt|F(dwJ6vgS4P_y@dLEn}sf!rhOFAA-LW0 z0GR%}gI_6%>wC$il}Voq&q{C%oJq1j=$1C02|kKULN7SG9NZ!N4ln+7d!xjNhKyYW z2_KxG2jEW%`6uClNUOEfa*7nEXkuoO>+@rv>(?p2jcVSlZF{H2HO>sHD*jHTJXpRa z-zUk$9bRyi2>Mqah2NkAiASAY-~oNhJ^>0O`BuB3*CD2JIUwN)pgUAx5W*Yma|uqR z(;s96``-F=*Vp-q4yVYn&GvJrD(PGU!%EHXs*khdBan&yPjff|g=c;3)iDG593rDw zR(zFmfV~=m^#ssv{7~1!;c+|H@7A9@x0}XuKFF)K1oIpYxa3Z>d!(GJx$3BtFB(IW z#o5eAr*Iwq3(XKxVqS(QhBGVLSk40`tFv36Z-=2-(|vzYMxbOZr}X!9+vvTj z+MQD#mTS%W$=W={Dt4f&e21JvZLJ2XaE+GBUyNssKwE6T?V#Jy0Vz%c`1)*3;x1Sw?bLHv<4?`>ON+Ca7x4r z--`MouO;~Q{+LSe)$%jLT#wQlL(`oS)j!)5c?zfutHKw5{a(L^!U zy-a7zl;Akjc_bw$c4t;ZO6$tsaoTCb=XznZ%(K16B8zjZAHTW`9emR|$q>a9?;e7c z=5iw-9czlJuu1xCf3M`b9~od^CScl&W-`H6 zt)MvVdRW9Q|9_E8)=wmUPEktJB7k&JD4K!L#;R?dxqe z!c4j*PS~T7|4KO<;mmjZnST;q`ZxYq*wpy4L5Ff-aTzVNuc*MCqNhX^?!ngs%Q9~j zRXJ}WRV*_P_|K|VKrXXSqx0xlPJ6#c9lXwHwtc$C@Bzm}qc0@+GucePtEYf;sMC+t ze;#e|1_53x`=vAnT_6vS{5TN~I@C&YJHDPqGY!<#&ixQ&@g_aOQCHyYW$cn)ZnO=@>#Ykp0NTryA(nx=;Ru(-{`|^o3cI@kL_l%09p&MO& zJDvvCPM!7PThC4Eui*>QXVOLU~P zj+wa&@P2p^@4k6d+iQK&u4`hhe|exn(OR2&H^QpUdwwINV>j0Ie|!>7ND@@M1nHW*N* z&QVisYoEDfhcFLdyIAhwQw{$zpj*jK)WUVo~%@;txn`Vc?9o|TJ^xi<@EOFa1dvJFb z_S!@9g^I?!r-RzTqo1_Glql-eR%zW4&eo%HKUFi8QmAP0vJt`NQj8aM5 z=FIxKbd!DI4AW`K2T=|uh&gaY7*x8ny1x6U@eTe;kG#c75jB0Z&wHYlYMf0)xLb5dD9i0wc0I%J!#I6}Pvk3@&u5$XU}_ zAmIk0c0A_@#?~fj?Q$sRxzP+gr{p{RIWnIoC$L6(KuK2{`m3u+tBC)LoTlci*Uh}6 zev(&KH_P86twyH4m1C17TpjJGm5w}r85|1&jhB+^97nJFI>=~tT8=|^OqQ60stiky-Ss-vf4iPvYNPlX1?9NP0h?@OHlZjH~?M6&>xWCjdBHJeC~fi{ssq|5Ioa zUf66fOpu3vwNc;T(Xs!2H6>vuqQw*O$fwuDN3$rY`ZiTecng_*g-<5APaOysE*`TD zNZFTA+|d?K!N8i$l_$a|Vc-hBU4?K1OpEva2sW>xSAmkl&Q3uiLOMg*F-o(1YsLSH ztW&3N=29ilcmLNZvv`lZiCaNs^5F|`l2vG8%r$iUH2hEkex1V5g27;5+waNGv|-L( zQEx!qjV}OSbjmDk9)YqV;+5#1>^=4&>+9(jXGDyL!#UV_#ke;ZxOrEy&zaP^Tk>*8 z*a5VG2q?)cAg5)?D7H<=HXkiGqm-V|^!K><9=#vocd98DQp2D?^Jr+QBCV^pWb{`( zr`^RmkKAf?9~yn!*z)Wb7~*DZU8&fZh`d%IoC)k2$R&8+#T-%4fU|C<_t<{C)C6Tf zN5Mw*e}(heqO{|U@JSclsz?2P!x?D0u($is>E{0H@KU@>V3cD2w9UmKJtye@YQ!!= zp&cz)v-TtN78?+^raD)(74OxeW`Om-k$B_hqv_#Oz3yGCpDrminpx`PnFb&3r0W{U zz|;tzt1{x!+adMfQ=d^x%xYDHP2#Ta@J8WNinIx5sRO8b8D)5@DcxJC$hp@dDdiH! zl}Hkc*7?tmWjO8KF7ac6?|wN6%^TRcw*XuRZ$tKIh8GBtaZBY1q(zS_i8lr$B^$h9 z6K=by25s!>$lvDboabEY-5WO6UsgFm+#>bAu_}|eml-5n++t|~_{OLS;w{IVP;;R0 z97=?mF_!`QtShIPMuUi|*tW-7X3|$7rOPF&t7B$9>TwMJ2po6e^>eANQ__2k=+Vb> zUYH3eV{83zDW7Y9q;Cz(37)8zqf~sz^Ssl z_ZtXq9Xj%*UdF=Gl`(k8=x-e|gc!pCc2$S=wOtRT6r?8cQb6=J+0hEL9-F`2hY9-D&$Z)VNy0u|H>EVc*^zSmmjr8&>`LE zDTH7uQ^-@)bMa>gPyLCpE9nK~5icmae6sW>D@M_7DKi0{E=d>|uiX~21Kv+c9);g4 z$QY#$H%?kG!DlowM6i)um&(HITuRMwdZJs_@i{PVq=6@#LU&suW#NTfJDvH~hak_xd9vFVrQ-wTBn zYqVVhe9v>JkqMAFM1c|uGRtDpkFW4-5cNTqJ<+pvKJJ%s5c$iGdVErsPP~3{cmQ1e zI^wJTxZP;@tqyFwMNFDcB-if1g;=@Lq?U?jQphF!4A()C9bxUjt!IpvemD!hs~{dvMUCa8^3fFZX*^ z$S*Pmk;j<*ugb;v)0TO|E{FwoNTf>l*CJYqzkmF&CtbKVRnX`-m*L#s>2C@pI>9akwl_O6Kcxs% zJfp5bfApGg3g;n)r~ryYir>RzYCu;&*PEteBCxdWtf#})a$s`r&lnwI1Q;b$K$lR- z@0dCN;5PEE@gHxsV?WSjJ5&><(|CF+0(_BAkp6AuO>%*dO())V-2!UriMj06Y1Km*&I{C#B5S2Mp1WwT=&v7n8 z$jT(B57GU;65;~F!q58Sc=M3mkKuF0OlJG@?>*OArCfSdGA6vz116(YqrZB8jKGK? zGyh)QO3x>EiRiG**rW@)thztRl%!B107l<#LW5$N@Rjf>6uni(HJtFf(}lOE-PO~{ z7$A-ziM$E-0*Z6|NNS3@4E`QDDu?3fg(ZSoWgC@em=+!~5P{v~&TD&E|E-ulH!TGy z$Mg1;-U2nB{LJ$@ep;j_{tpJ`McAyz3Z|Mh5srrYFprx$oAjIDl|+WS0;S z&sX6Jq_`pqeS+I>#L=pHV1*uKh~T%t5M+yi6)!c(%`wkB8<&;m=w&h7+$) zIq-&fkDrCKEkef)?2iusmeEQah{Bbk}Cox(4(Ja zeV*Q5qKFe{VCmgh^vFZ*&I+uw?el3R` zq!h6bxeVR~Q+%%=JY7usKZG7Er}rZ;6G4_b}XOJ zu#iy`Ap%75o9%d1sIyi@>yIK&%ls(j@z+6kL{P0qp1Hu+QTVi8)YDX8^Zj{=tqyH1 zKm4!BOd@Ts|Bz$|t~IySUVg&xo+a{)6b4q<>A6_oV0f~%+)mrk2FkvbngY5C>fl5cfvp?0Y<_VnE}Jy|fhxA|%aCP3ay zePUP8Fx;Xd>6_rWURV^%FZO!e{xQ6Z{WpSheAQ}x0VOQtCzWTv4UQguUMY?_6r77n`VqAWpg+7_O_bK(qkE2c zO(`s!Tye+f_2otJI_wzpy{Tf1I|l#pcponIslmkQ58xoO3u3t4jTwwc7pO;Si?13^ zDN6o)(54u3PamGyy?Y6{Tb?X_a$3jeUl>0ODiL5&wdsA?z$<;JmXXPr4aHE^%l2*% znQ4x}d+M+IhiE~WCPUVjoy{#yEkVtpJEmW9pi!M~BAWLsw8sxNU@_rCv<`wLKlH@_ z#EPDew~rMX7d(0B2h$P@emC*EE}_Jvk={oKB{!;XwhvzMFne|}J-Vsa33;D9>`JC_ zi$^UO8h1;GJ$!jAK5p+?0b19NZ8Z75fn0U59c%Cg_z_c#KJllc!Cs-of_7C-maJsXXx$9C!vxi?3loz*G}((>cYj? ziQ%WlYCjs|1oT{EEPn?0XueR)zmAIn+MB%q8ghbswtwu#DW;S8+!s`Q^a6R6Gkpcf zH8>eO=Tu{V;uDuL5DwY}JmaEn4C7%1-jwduI6|YryoWs{5PZE{ElA+A8-UL-LM8N| z%wayQHHJZU=co5@#An~SONFmTnDOo!+Ekve$Un3@2yYg+>59sf(C{`D*QmGTP8ZAS z0)XfBIqYZ*=@0s}2bQE%E=SD4vWoWMYcnSg$~ANk^KkP7d>ogHUa02Q(DjW^%dp+p zH7@pJ-2w4ae>v>W2w!Nc^Cx4^mQUhMXNm&!*s#s}f!>VP03`N6u5}rLAJLMUDR2}2 zGu5TSftIfqj^++VZ^@z^>ACKv7N_~v!5somn?$^RGf7$EzqR@rp@nCpr ztj*_z1ow)AoikjFkGfHaXsK=uw2QNou>`x*z+;Q(xNG%$^xvIsB8%wIJ*$}-I}s9k zd(%H<@QoDcA#7CVH_Cti1}ebgx!EHW!3sHRoNcu$g$VIpjdtY9vfkPMa=33~73z@A zj}f{<_FH)I)PERLiRAKG@Zlbb{ujr_{iZ2iFs3RduSIkIE;?9p-}C^vYeQSB^>fBg zg}eo4o9aX6`=}Xdnn?#Zy{VkN2Eoq4nduAXi5#6hU>?i6a3Aio^q=NIL`TVC? zuGOVZNz3xq?Fi|8rhE3`i{VFiewQvZX_l`m+(R~6ukWuU)0!5ioz8mL-Vpm3_MKUD zmKfkbcDn*|q*Wg%crC|obD-t-wVpH3Xvo!E?M?^3+i>BACFiU27;a)LdFl4WW=rG4 z<20rgLN@Sy@8XC=Pm7P};Yvx!qla^q=QYVN{J5b+C*dy6@mQ;e(B_f1e60*lKIpPeJEY`+BWk}y zdMEPvGp&&bwT!HAcubVK_}+EyasKy#T;FQsqF|c!QjDSU{ab5&H>nwzqVI8Wxl<

%Uv`f{btoprkV{{&m`X1Lgm$cM(t(lX=||zp*Efd2=^)iq?nb#uF&q1Gr56o65JY zr61QlQM%(f@;7uyqTgV>_3ud#O5VTX*&1>u0iu-;b9*;Ot_QE?N~muh^(46rFp+?s zYyZ0p;4s_M&fN}!ORrtv!nU6BBA}A3(p1MfTvFhCNx%=-t={a7M*#gRn*T0$*R9pZ z_n^TB!J#8$>-B@9dJz_Ly>(JstK8zvoF6mq|67Oj$c9-dq3zPCAdm4|9sDJSJ zw=JKH$CjS`a7<05kizwrJ`(h&Yep0wV00{)5DMmg;kxZTb)u#Y6m5)P||wjCSfG;%_!lbEypKyZbc?@$8FXnPF|gg*<6O_OQd9NRSI6r)0jYOn7% zdL|$^C(9?Kpo^GI6owtQy*o#k$y@#(0dc{qY_@t%xBV7_v3@vuu!@yky!;;5hnxkd zr{7&(f505s{i^?!gPro;v*Nmkg~+jNJ9p!Yt%`d|Zo8QRyzQHA|5P_T;``a`^gQzQo1-**8zT9XFP-(^Ios&Gt-9Ac&`fstKqwc|9Bl7&f9b=1pRgl z5ct&V(w*%d=mfpQFAm=~)dUVZlQ6h0t)5c7M^a;3&eC5trher{uR>S+3U(>i@g7Hn z5{!>vr&Hw3r*%obwLs%|l2YomQ<400rGUR#b7wP~{Im8SlHIgDOIZa#BLOM87ax)b zDs5d>zA_XvG-jeZLMSLh(!aeC%Y7=@!qw;mD;vw7w|;=lpPlY3_~EgAs=2B~IbS@8 zoEZFkdWv`7lkrIkTu6Q- zeReV+mQqM2@vNC>=*lgF5WdN$|8RsM;$wUb8$+PdCxIrYvgo_QkKv#7Gmh^|#qNA_W5 zH@Wr7BUuMp>-4Q)Q?qRWs6;8JxDHD;;dS3UjD81hKC!=~qf)$X*eU1BjaSGR->Mxj zRv?cdLBuE#MCV%~E$@4DW_x$kIxe;udCBib z*e`l8AfJ;M)B?Wlk@+Lsd!-l3D*Q=ql`1-ZtiRn3<)bo;9WJ(EX-*Q=9SrtkW0zm)tNl4qzg z@QaBrZD>53jO0E{A-|k>+)!IeDZ25}TyLYAnvuO~f_T$6bR!_5<9r_W?&x&~Mo{9Q zMbR|X5lU%90qqhz=HwMdACh!_`1U6&1z1Tpl5Go=AQmjYsAdldm*YZpIO`M-)xrJ7 z{Bfb%6^-2GlV{%FOAA%dV>62G7MTH_R+@NBMZj0fzc`?#u1AnHpeX+-*D+Msy?0dA z63Fagt-eIXz$(`#fQbB05y65IA*_w9mv08RtY~dXWqOyu)7|BJ9|}Tix{v%Ny_R1Q zlu@fLjjne&=h7L#DtR$-mw3B`v&@0y4s>k~zs4{DlF_4mEFeP4rQRlkDOE7$JT;vo z0;&8trjvz^q_(omnC+Hu@c9|H!j82I@l;3_N)9K05P(1|2LcBpu|AqMhsbjSliRlO zdZj$@&>C6`Zx(DIyo`0M_OLu99zp@@+t+9<4=2F5Xx;ijIixvH1$2L&ll(IU9E2ka zr_B8os@IFp!&+jCb7#Evjne|?Ql>H87pd65FZynAOzmPxsJ#lxZPZ9B;xqDAEYWQTiS$0VzrKC#> zld?i(@0BSg&9IePmKkqfX+gF8e%$%$x|Ws}{_-5T@!&Q0OeFuT51fAR^B1=qY36QT zzDe%4RaG3J0e7f}@l$(6F(y1U8No|`A%pfU=ntErm(-t5ox4>Bc2S61<6m4?p@MOb zm}$;d-1!p&shM+65&((<-s@c=Y@NQHLtFYZz^dUJeGu5nuObYr1frQ$hBUAOswOif z?V1(l3dt^1K*dRMv}UHy^LL`xv%e-1BZ635C{%8dL2( zK?G84{n1oFJc#!bK&20E<@LU`&-i~;7@+khsRqd-7foOLu+xrRQdXL3DRo?`45HNp zH~q>;9PD*EnhBZmt&qy4qow~5IQ(L87w848_YzoPqs?5^HOLxWKY%^R-w3Eq<6e`1 z0nk9!rA6!y+z0ICE}GSf2oQ~kTT9$k1kzIB6f5%>58u*j?h&zm(ur?_`QYX1<=j)! zMELsb-t=>@;ja)dIlVahx9hu|&9|_|!6X1Q;T|BRyUjbqEgr;(^(kTY>Oe#O9|rUT zS!&crVXr33Da}44I6X?tlpVmp@f-b;ebNpO5*C?E0MTnt=W^=AW2G71ME3Cj8(87J z5?We!v5`JwlRAWT53ol2Wbh|*7>t$}n;ae=|01-c3Jcr(F2lBwEJ}V*_5f_e@}8L* zPIGto9j1G4s_?X`y;;9!?8S*mr&)<}=R51~chsRNpRhltcV$JKTu% zgu2f$XDwJjj9P8NeL!ZdXp}kbz(Dq7k4Om=AgqO=IoR~-w5s%_l$#48BfhGMTa$Nh zc|ytWDLu!UFGwoZXQcv0Bu4U-e1VQCwd2B`M1RWN_6$exSKCdbu*oNySCL z(9zI-#)95-_Fx;6?{OkqT)3t8_YCJnd`YNY<4kvKNJnbcStg#dVWkSIkcTa%`gxKxkdR5i;_|*fE$=Id}_eQe>g(&%;5>sJbf^B}7A? zl_VuIY@6zov5bIi_rCZo+;vscnSt)ayeNI=U+uTeN$Kc)0AoiRIL)>qTK^0-FHkB? zv~}6Pp!sg8I?H3J)Wl;)ATco!EV6M`uQ1)T@zUzkhbz^zCpxOrW)mxXhIW8H@pghN z(cVH;eOSCY(w+l5(t^uBkGn?J+fue67dPZ6quB3wl6|~Ja!zNF`fUxeRFKEeGz`4F zw#}ND9Pi={lmz@i$dls&542Hrmp;D=LmOTI$0&B!(5DP+x6nD?^N4>WSK>#+He@oH zfEasa$exTa>Gw-3SEE=IcN#J>E$D%q7bzdDkMzf3XS0VNRt;=XUaI`~LEFTxPUa!e z?g-6FSWq+luzoSfP3Oe#_T_tXYLR|LY*_L`)YM}y2QApvQ zPBA(W*I{`$Ya{)8GL;{KJ~tNmtwjq7#XU!-K)3E&RcHz#roC7Z=~ayYjkwGBT5){u zIf&eW4U>QtVOwDL9xJ4+kJu+4S};yD~ftMuMye(^Ayg2{^q^iY`(?0 zq@&lk`(>>Pl7hYr$oHJh%JAqc4VR`E5Csvuu_Q(?=e$Hs>OSnSN3Cz~t0fIWSp4sG zh_)d|4gcdsp^$*2uWxpuL&XgAPc5;2yk6m0;Bbi}xBL2~M@XeL;n=Vzb+$|LKD8UE zruUJlnvg(5%nBMBz7@`FHrF9RVo|=MV_g4;6|n6&>+JO3Hi`DI^A1ncU`$_z@R`!A z@=9CQ324~Vz{Y=Axh=B0B=!%{@V0)k1#q@dQ+6NfN=33vrhQ|WTp-GAS-HB%A~oiO z>HVS&m`>Urh-iw2$>O_y`3sL%duvq(s?eNUhVPo)F5o|^p$yMB1>T;0!5CS*>|*mB z6QD{l!rI;QTkAHWNu2!U-xiO(Pbb@J0Nf~R=oq=58$R{m@#0CGUhX{=x#fJi5F}3nxWv#QQ5BkDQoQ|{2pQx3LvMG7&S4Q1W zQk`<5q2tAQrc!=A;X$EHod-makg0%zq+R}`>Ec3GK((A}nwOPnXi>JeuxkP81_GE= z+xzDMf&#DT>b!M@i>duIz%9^T{ZicWcqWxV8FUwC?(ur7>^DILKkZ8oIRdczU=0QpRH?tuB|??$Zn%XpOO1= zDM7CArv0hR_HF7}17kBq_W_&Br-z_th{W%}ASR>56VomBa^R*Nic`eRzW5g<`yNCr zsK{B`9Az+fv(19RC)Ojz2x(N zulPt&;Ncq#JMMEA_ub^vq~tpNFPT1F3%tw64c@H&aH%StSU_Z_+`Yd>j5X^`zhN zS>Q5}-jE~S+2}{R#Q1Z==qIA~c;_LNTQbbncJ`52!x5F+CP^$o7hK zIcC=VwcY#}W6gsG4|>b|hW0F(J)039xxD*No+}1fK8@8v{UPxgxJ8YCWzRH~&21O7 zIt$~IJBM{a9dnVPLG0^r0*WH~$lq_KBW6#Z(bO*4ShgOdg zq6t@q8qxICC`8DakPi(>rZXcwgI4*wiJDJ>`QHu>%p_IHCvUE4*Ya%j#YA2vj|_Y_ z!-HRe;Fmef1dqlDjq!DJ-Xm}9;C8YkU?b9{ZI#B^QOuQC07EbzqKP>N(XVNhRQ+U9n7K0QcI1xPV*LT_gC4Y4G;gPd z+3su+weB5w^7tsa-~ENx-6}s8UsiLJFHjgCmpDT=Vn-%DO z@%xrFXTF{9+)^rh+ZGI#p{OfN{=Fa@V^Xqs1HBNdz{h)>>L!s)+&Sy%(l<-a6Y>(R z^FW7s#S)DQu71_`OiEc#YV1}>^__hf2W|nsJC@Gl^lghCQ=if==N2)RNOk1fUD(}^ zxP0NOKfgSqF0WGntKL#5P4v7Qr^j<+xyPH{qjbDuWoc-+$^`a|5K^V5FdZa z^PDBB*w&X)&lvsQ5cGu&Vqsnnd<9Q9;w%|K2l!*m2PaD5x%b>j&_Qby4H5DI zIizxI*yz=a}rcDd!~HdLJWiapV2S!_;?G56lj zyg4TLy$fNF>Vq{6pDrGGg5)y<5n)VBBqaYzN#Tg=%Eyl?A~?ve>D7YvJ8Rzl?(fdN(QRk3@|Nav2gM1zf(-^qOva&f7&7U~^n4PtS z%Fr&Zwp2^T@7{x-JgKQRZ?tN~7iCw=4rA))N&+e#>;4fq9si#fBl**dww0>mh`05Y ze56AZ|NaGrP8Qng?|iTHWj=G-SHM!?YzBaR4hmNS9TVEUjFM!c8@~1 z4P^ud_i`P>xNU(Q*o-1v?JV-Nf=*rDOAs6BEE;=n{?_>6DF5$-MZ>=moM(3#$Y3BK zeq*p(JQ&oa=QoHQG-)koZ1l4i(R2r5p@ifUe=8uGt{g66SFdprwIh~RHc_Y_R zBr2uB@m>$%Fs8lEs$AOee9d8fV@FQy+(aC+e{|z3`6>aT(IDURvbV3?kUrnFBXddr z*aC%OwK`2`$BV0UEm)5MByYkkhQa@=$U96xDDPV0C&wv}J|lGya7ni~juhJK3bcQb zQ(i79FW^i2#nq*71t{M?IvDgv2ZMkfq2(S(MyWM41YNX!s>jzpD~EZRue{B{1jReT zOOc`?mVc7ANk`geAMRd_rX{2t~gXAVkMnD85Y(>~DqIGHx) z%b??w28I5m?|)wTksg?pIuAYj?%c8v#0ZX~jN&s~w&Aks2gadJXryut=HI(Bg^Q&HBI{5a8L zsQ;h%Q4t#gRle&`7}%`HIGHOJ+PNNO#nVNpuH|(7)Gi~~AI;}WWY(KLvfc3`6}se$ zd?Wackkd}y!L5tWAS(e=1!=leQ*Ho{dSZF0X?Jn%y;_b5LsBbW(RpCBlH3H>FVC?r z#xSwt>)`ivIN-$;%0#Nxf;r;rf_++VZz}t>I}h!YJA^X*mY7$EiWx%61&b_dg(6fk zyLaSzE;VxvL@n0wp16+~KNTNEI*LALx4V+YdQraikM{#*ZT^_BIB%SC-d*+WcQVyy zRDHm+nrHM+t!oK&+!W87 zOwYI2-df#is{DNy#}}*Q$g$h*mwx(tkWkViy9B$+AJ3NFKmPuOaxp3}WN}JYtre?V z^s>mwd}MsEXyd0(eCowq-1YD7uL_xhU9cbd5lvb6tk0g~`tM}!N+~_(HPUKS2;WI5Ol2!#y3gb?m6*p#Mc13IU7sF|`N3!ZHSBQldx*e7 zvTHR}?kxJF3Q?3~>m9L617DKwO`X=ll@ zMzyOOqno&mTgn;yd}%Ufwe~G{Llxm(OwBqg9+otrP@}ZkJVQG<`JHEaEhokS*+RQv zq#RCbgg5rArv5{4&QbIf+!Z7$-G+=-E)@Xtk*EZ(rvkI>mDO97u%4L7q>q&-yQwXGBK%nIv|4&zb z4W*YS0dq7jIj5Ve5fbZ9X``dF1_%4aJ$6g84DnDEk({AT(D!Tw?>CioUa}kWdGEg9 zz34zF#+ji!)X(|6mZQc}5ZmKPq4x5TOmr>$34a!O2}Se@vdve7XLLQOiuhg^QKA*X zt+Qj+5B&%iq!(ltS$uOWGl!r1S)M$h&XE2nq80_*>B!O=7~a-hlj$hcakTf+WAe$0 z&sCbLdyA63_V8LLsMq%PxyPx-1Ev|m_4!BFfv(38XJmc~0@9rmpV3;%gf|prp8_^q zM^CsX8SKtN?a)YnVDT1MpxtLPPFWWzktvSMGR%}mU39K&He?Z=H&%^S#&u`cDqR|sWU_SN#PQgn(uj!-9QFI@xZresF=FEI@R;Ux|=?WKpUk)2sbct~HBn zRE8wEM){=w&wH;{n5O19&Isc*lU5_KdKroAH+c*?8ENS?4_dwnbri>lldJ;Heug); z&XSf}TfqTXsBB2@Cj2^h!eigXfGD3yz~xCB4h6o&T^9T#VhUNT;UsU*F?&U++$*ryrCME*^@p|XaM%IO z77*ZUHZiu|bb~e#z&yv01`hYldgm>2Bs9mo1@7Lbo9$K~j9(FNzBpo@HzTI2E$uNh zMxUzQBhY{Fw|+WHgciWO{)Q=Ax0h&>@yXS8N#LB5L1cHcK$?Cgf4snMzJ=0Cs0U@27pwOZ=7+EqZx`e) z&hSD)mwnY_QlH%`vet=3jKZeli5FqN;fhKa#`wj^v!^1OsvQ+L{f&CeXF|WT9ds1KwMNG`BdRMB@ zOvMYx#f5PY)WU}N?KR<<+fdE3NuMf~<80Yv*5J~X>3|9bwTK7!l75u6&C$Ef&m00v zFXCy>Cr+yDCT88@=guAKzK4;liG*og_c@$kS#+#1lkdLV<>U~2TfmuUu4fF3>G-@q zR+N~!;;$?lgh)w^5Hc*b_{4HU=b+*=SN{_jYei7mBXxbj?bC84C)4;epRm?wVkS#Q zg)?Kshh2daM6wuGx4D4mz2A8)#f!cpEg$*nZ1X@F+Kv0AE>6My>f?K6od(8n`AY5- zPpFk;%}3`6FSpv1=9_M1T}VttNNbok;36P`C`V<%O%lhGH?p--S*(~i`{#hM3yXQyw5JyS2#o=u=`>td7()5AVfw; zlLY&x;gP4q9{(nC_|Xg`zL>v-Wxfv2hIiB}ekG6MDVYl-VcK8>EE1)F(EJEnPj!5k z=!nEOGT*(Cjk~xP)VzmM_aWq^m1x7R6i5(;kN$>JKXD;TT2}q|YR2GGoh3gxI;OBiUq%2Z6MzkamuzNH6FsI@0Jt7pJP>t52qgGyxc)(Z-Qf;&SRUL9`s6 zvrhabvwfWWFU6nb5^6bc7$ynQx~&Bow!c9p`4-8LseJGX-T<0{8%KMsWS$w&cyFxS zcDkTAIjLjh!T|`8LcE|Usyni^*4G$Fq39~y`;xjWJUDizHR=>|PbkP#nlb4w84;18 zH)(V{8@EQj&$&Jzp4b@AX;#(d|D2uue@&frRMb!R$4OZlL^^f>rMr9Sl1@=sSdmTv zfu*}sO6hI{K{_OqT0%m)8%c?!;kSIBpWo-%zdmQrnLD2|b7$s$?)!D-CdlpFT;i4u zaSc(XN;i@daEXj>*6`y~b_nG9WHvcf+HwJA9VzCXZ~7z+E6aJ|HHCS5Fu1CYWQr?| zZ*hVuziDx;!-lVQ^|#*KyD!yrX2~vGOQ^J6wh0JYjmgI7b(w)(MqeV6hEV^Gl8D1{ zZQ5y{>NshqgUJmwNcDKmnyb(=kHb+7a!xmN_2-b4F-EJUWq<$I3*By6f^V_J`fZqH zQ)LyG2dihHQ8U<&j6&343{M$Zw5o~Pg#D78_g3hV<}&xYJ~?VG$M^ZMQxl0j9yZTJ zCBDf?KeX4+7Kc}vQ*0dTrS!YVoDJr$YT?wTIDpgwJLIbe8VrlGeI;_3z#uq&=+P+; zo~)@fEzsk|{u$8Q$%FEz%WS=Mn^Yy@07q8%#deJKOFqYXn?ci7Sl-B#Z_(!lciTN4 zXC4>rNIt+Qy}vtss5pIdVh(IatOAm?$XHYbmvRM2Lpf$H>iFVk7v^FdT1%sk$w2A& zHLRWfV_%*7C9aO)+&i+r;c)uH9V==#-i_zz^drf6F z&Ja&zloV>7eqN>$ew1T2wiC-qtuI6!{M@J(mOIL>^R*EkKZOTf1|epQzfG(Vl-BY4 zD^`hm)wEByN@Z@5^t5U8mcd@0?;3FFD?-Lo>?@0tzX;pTD4GXXUfX?Yo;+AQrA6De zNWz{8#xh{>ChBN_T@_AQk6u+JjpIh3NVDFwB|uBn_h@atQf@5|eLoKh)1^KZAq;%o zL7~~ifnI7ELACI{0Z!2^%2_-qVnwn3v;2#ltJm|O$mu&a_X$_Z<23MQ@}O~>apqKw zbu0?0m^<#2(Yv+blpnCeK*<59UTZn9owRa538)>RG^@ zoRU2C@fBU4L`D$1sGMdsYe^Pt7oMwB=%uekvn{DkiOjv`b?e6Yz4phUlE@+VqINL? zx)*AX1A*i#=1%uVEa>>;QrqZpr}`4{WF+R>$K$-4z?axbZ0@z!&r;uvzk_|$+o3xw z28ta=v5>?li0nhxJ4&V&HTr(qAPbEH&psR*KuZXwc=SSf(w{|<>!>Y=CGlhIfZ)ii zN};1l6tlvDv?qllRovqtPj_cAkN1ticlRpp+P=XwsM{2mqI=36h6A-YhK(7-dX&yQ0gDGFXAq#m`h~fVsgGXP#)1!G376_lT~-;e;4FC z0pat(zZag>#i?RPURnj-)EBmuEXz*y@c^NS$`tFwnKHh90&$Dp#NUPi;|})p?70;k zd93FWZ;V?#WHv$@+O;D-P?9L;)M9aSh&`gHgY>(2$FJw97hhM#HOJ;gy;~UfD+7YnRCN8;K02o)owo8{sUQ15czr!ZgEGAWB* z+}=;*XWa>ZbqJ-&bD`wGDdUY>X`81(^Pe_n-|4}ZqXd}AXfCGF;mYwWN;ehikdm`I zp68H<&Npu-Lwlq=_qQo66q{xe6DT_y%Z#Q@obbuIAL9Tqa(Jqx(IbbQIBB|MsEWSS-*ZRJ`E)T9^+fD$-LM8hOS zf4wrZD5lw!lzPwat{U8hj!LoD_c%+n&)F!q z8|7Mqha9|oT)Ou4k-1up%#rz*f-jEk5Fom{9W0x!ZeSLkBVG&i11r;nzrb?i(n3@$ zgb9>3aqn_JW3Fj#P@Q>b z$JmaMhPZcN9KHFWn@r3EwCUDd3QyotyK!^;diH(>3yA9BT@+1QYT!f5dq5jU>fah* zTH;UiCa7mP(H9lGttyVeZS`ALrj!xoV|dZ~^5QM19gON%K)V75Ul$A%1aggDFw@Kh z@)D26Iw17gGx*~=+?Tv7*9l|!-FAFp9P#)wWO z6e#P*LzFGFjQXr?CO0wDKn^k2m*CEL_f+t9E+ZI-5b<0aHWcS4LZDsth~eI50}p)D zcN5qh{X}627 zTotk;CQ9nIuwVUV>h>c5N~xLn-M$z5ti=<)zf@-ym1Y&BNJwXRe*dfhSDuR7w!YXk zdR^%!v40pzmaNE=oUM86?jio-Si@r82%O-4Z$RDXOzniq9lB4@g+&F0ZP;?7KRWsq z;7Z@}*B=DOYQbo(f&mtLXKmCYN&?DCM#&wEw~>Jkq0AZ&DgobMb@1Re<@_2})|Zp~3Ykhg%Urv1Ny! zjAv|QWP!DWByKVX$6@!31<&k;ODoIP+QWU;xNYx@10rR?}i#@KV zd`pzSnpHEpoGy3AGv*LB>rz5awwo>mt5}^0rzKhhJOXF1RFgmtjj;FG9_PP)|LfO) zP={CJu5yN)Gg=Gs8x36Eiws`OK9cipew;3JHgmyYr}*LL(j5R9dDQ->iiqhVU^zx9 zjh&<68t=YG2^A_NaqvS>QlfhID28j$WxT`3`>{g^(a^)9*ZqE1vT;E|#;U*Q z72R;!@!Xb(Ssmf%RYKBdjS0}Z>F{YGVIWfzz`wwArY zfzD@MtWm_Qee}iSbl*ro^C0rX_t1d7_Nu-)+q)LnfV-3Wbds1A$5Q_|)R$3z;rcbN zYn~e;^rWm?nG;76qe=DxZ8e&3?^*-K1#N7wnj00w!M;Vc?9^>LFuW?%Nw(nNipF)hq?f3koc<_FuRNbC?SG-L&!R#5crozOeg)LfsT-s9@*EpW$ZNIgEQa-Dn4eGZgu zh;de!<7*f~9gcb$FZg6~>Q&wv7CQEESTOlP{=&fsgZPjT#I$D0r&9lwaig;KmCcrr zc`Lm<+OX7H|1?3L8Y+AL(OBX&IzEXDf;_=lxV8I+pI!d^2rT=nao zRxX?{4Mw~tj4U7bB4N=V!+2LW-1S3vz?LYxbZUd4H}ryz0lPS?qS9V?k_B7dZxN5e z^C-lfOCtM~`UE|$8T!P=c4ZBB-AFYk`W)XhKz`q&*AOn?(ar?TLDZ%v?FDG)l?y5D z9?s84D7d;HdqRmwW$prOI@CeM9x$I&Qqf9lJdQ8($j7p1&y3zSl)6KHxikAu2c!P< zSX?Z%J$UD)3Ve9UgBBsVlBe5NAYR2-hhjXw4Car)?m?xO(O0vz|}l) zMmdx>Zo)IobX|HThq3g2=1cF}j42aOMa~Lz*bn)=@89JI;4z zn}87%=u@k)!H~~AL?q!qP1UJxanAfi-cO*=z35}sA^UyqRF>5>1;#L)XBeWQ%2(f= zD!I@aLhTv$Cx>Xi%eYURlcX?LHgJ(Bab}7;JWz$R??LPJdhQt%*GHrb^f9 z$pip&0z;C3dejG!)qb!p6=uAd(Ql?4OCuVZSG>xq(Z>bc^`ZD6?M;oxD)73UiV`oI z_Hv`zlP0mtd%>2IvAh=m?bv%8&%H)-81TXnuhVKo(O@7;VpHGz`#YiJTyk!RT4G8C zPs~rt%y>!Z0O;#JQ{p?z-i9->-UzSvG-9!Fbaw$l6;4cNdc?6Ihd*zzNy(s`40|qryOyjmx)H{n3543LhVT7J)5zvEx+NL0+s3L$!$5Fn+x=c=qW~w zMcJL;5^l4XOO~E-8bO)PL8Z%T-^|=L!VYb==Fos1j9=(CUAk-D_JY{;+dshL8&cAZ z0^F7u`K6pCMF3W9Z_ZmVTZqS5I(QR4xiIK88%=2=?if*wQ;Zs;l5I<7g~c~)7VIjk zXu^ci+)g9U@b_jTCN0%y=0-7^q2S0(cYnUWzb53RD!=Me>vWca~O(#&OiYW;>Y^YwA1@N7QmeEc7c zi|#?=S`<#C!lDcii~{4Rdc<7Y(%wP810FWE4;`jOc9^|kb&mVw+rEC^${!^3iw zLVpdJD$-(y9n4*G4#E7`o{eM_85H;Yj+71bSHVCWEEXCdPJ3MT`f>-tyVwqq|B?V+ z7$%?f0LzCWc@Ob3c_hD?4y)u>@A6~-Ubc#7oI#pXBb z)6Qg#Nm;>z#2vo_oJ=QL`BOr_i9TDf$>gcOa<$j&#*&oTa43Dsm<+w+WUTW{lzLGj z7T^vCUpu&zGd`Tzpw#+`=p<3Y5kG5!LnF z^dybEfYKBe{lV6&oXji>NatU(&+7A#LX(Qp<4pqGoNc|Et$yRnQXZdcN9&Nd5|}}R z!Nph|pGp3LFxOTGER^;c5&nEgocbp+iL8rM3MC+^iGlvQ08_nIc(;!pAL{oh+y{wP z6eDl)WA^n*3!6c^zN}@4L3->t`x+!ETy!PEKw~#`q_EcVj-X{~d_@Rq$XlxQ0#t3= zYoH&O_I~%eLNknOq0~EMKS6LG8adKi-0j^1+m4=K)NrC{i=raVmtuc5>%&Wt zx>ClE-0G{wO7VH6L`A}4O$+rcqci*3eXLaS^O%nFrGq?luWtw+T|kwpTRKr7a8m&& zmDF5fwBy_m3{x-koFNT({OozDp_1~)c+fHi8hh%Xu2F02pDe^5oXDQ0L~ZLq?oZ;c z@P)gntFVEDwI7A6UMciBGva3{Dk7)?_=`11XAAqUEMFWTmII5_il$z z6UzbSc62SoKTF{zM+KrVR=dn?;b6Trc>EYCkh=g1n(>GJ;EEPE)#|n#D-YFstR3LJ zlHK}@k?(ui6(>U<$-L=VxB=&Pso2ZtSL~8`HvuJ*@P~r&b zbC1gS{$vSN;CdAG<{E!8^z`mI;wjd`^R-w!4wHg6nZ*PB`GifiSW$l})3 zz53+60=~aWXF9UUNaHbK?7jR}>fPjgm6Xu-wt*MTY=Wn=tV&X>E>GBmcJL&Gtn|H5 z_3CCqHsJ|_C%>MWPyS{l_6{o&d|@Vf*aTmG%xJz6y8zzq8_qVp&$mhc629irz2ubl zVucmJg5hR&n-MB8181fhZIrb`4v-7CrnmhsF2RP@hN_u^MJ(%6_=xuLptZm@sW!)= zQzYU9p#P4cb@PdrG*~4|j-2bYlT5fMO(A?iP1I$hDC$vd?Dm5bt_6y{0-CPwif4G* z@Ac8!BsotZIJqPupKxmU+(`yEBfK+|qViPFS{AhlWaCiydW?`T0Rsl}%W(tj;DmP- z0QRQn+v;&iSrnssH>`IXTUb92hm@#1f91crD3i7W``xRwDO<2{Rz|Tyt_vl(0dqk+Y#U;hVgY8w9y+kg<)?XYc zU|SC4#QyZEI$k(Z2gB_+*wABBUHAW~h}uE2tjoDwfk8f?`=iGWYq8-sWPeGYJi><{ z?l66T+K5ORXP`18a!i1f<8@Nm4*|ZOdbjRP)E^6$2B_E#{dn|ETn`}KKVXs2L(qjd zPn^A$mbR?wWY8`ueYY#0|H;aEqdkzSp=ruhP^HU0T8<6xZ~PTdE&Bk@UD4=JCIY?P@|TCK};%B7&I<<_!`myr!~Bl?aF-q;OaqLSvB|Ly7D@NQ0ec|8Jbh4fpYgVV#5 zn<=saB;R%G)fd-dhD50qk1 zHj{o46ZcSacKyOJVM8}?=El-vheUHgNWj1DuJunzvRgNL}fj$FXhIFH^s~h z@BpS#k`$p(L_83xAR$G%2SSzl1o=sqk-`6d#=pSFq~KT{3%ejB{YlQDWf8^9fpvPZMb&hgjFkzX0gfN)DG zW$EXhS z{ikzY;M1H!iW5M!p;Q+BsKch{cosaDlc1U)!~9^r$z-{h@X>JRNg_b|gA?`0bf3|` zTlPG0JWsmQy{ry64&b^wS!tRj0c&54683DRKT6yZ^->9fXB~AkiJU&AUKpCWfNY|8 zVK=-((Sx%7U92K8{yP`1ig(#>Fi}HLsE?Hsu3FKZXz&e@*pry~K+=YaiVC&_gZ9tU z78f-sp5$rRcn1#n?+g6H*Z$A${|__UK^t(7Rb>5Hx@3vF69xHGQP7b8ENdG0fA1;E#2MST_UA)H`3kRDczl$X4BpI-RR?aJb%DD zKI0wZ{J0DTIpj5(x?l3ROh-(`P6sm{TYy=tBfJ$aiM1rSqYn zkob)G`K3kp`H7`%EDem!^r4`HQIlgF4Hc+#v%LyAQLF! zB@N^EmBQiEGeK-jDg`~cR%jvr*SZ4O$Ua|mrV&ptq`TT=J_&3r2ALur7BQWI67IW| zmuA5=)3b($?$h;9(sy3~?RHAgUW{o<*ay39YR|FyJ1=4IFrd+tpj%fA3;;q`pI|2L znz-WQbA`JqE)M9t^cv>hwn$UhPCa^tzh>blgM&g6m}KVkpupXLiIT1OiO=u|=sSz1 zV(@#GC|D1m$>BM6YD7Dv8pnBfMm z-y|oJ0JU-XW_}DOp#_5$RL1?e-L;A1BDRP$<}Ct_Whd8CiDircG`X^Qyixdm(X0J^ z<*7}h>z3IiLJ!${t{#_L`D2Q`;S|206y+tey~1}*I8Ya4Dv1OY&C&8umF94~8!LrU zhs01P&!8$nNvDK1eiCZd1Ql_Oqx+$wNn zOKd|KTpqwTM96C2ELOz%UdwJ?poHq#kf*=J&sh3u)&}!D4BqYs5~DXAJ4ag7c>(2o+%u36d_M#2;A3J1HR0_t0+9T|Yz3zVP;5oDLPqft z;zjan6QhR)c^e~qIl_36&X*sClZhVxaS8*dg5b>0C(Vit`;$K~3?qW~dW0eukJ=|c zjbEDUjMVOBJ%YN=%@6nyVn(=oX!oy}aWLcBs54O2UbCefuMmXV`TMBwc~fE^W7LI56r`WU7}28~H#8vZ&HCQl$C3@sB_7c3lZ zy$%cKpQas$htq|>=C2&IqNAWar6Wo|8K)bkAdb7qY=H5>`}#|k&SY7(5@jKe10pkK zVlZu|f>x-OYH4g4?2Psv$1Ak%R{YghRvxu@Y77=k#ef?0>7WL$-44_hieslnY7q9} z7xXsUBON!AW+cy7o`g%GL|rYEVTdXK(=X^GF~nqMWO5iU5x@xdUoqPmWxR8x>|Xm} zICrY)P?d$u@HL9OqL7Tl7V#s){Y*v`U+FC;V)9y@f{UVwB(j%XPsj#OnO8%qm@Ji8 zlN>#QM>^^a1-6vvD}j%*l5C=Nyw1GN{8^t-b1f#6^5g20Riv6exr@5L6N>y0dm6je zTNGIkZW_HC>q;&aH6801t4WtisY&7~`jCBB3nSCU+a~LsQL2z8xKi29ceOJtf0uChM>?>#?89nwc&Z@5@U_WMylv* zY71(&=-1H((a_PM`Qq}Hlh~8V`R4gj6Z9q&CfX*mCbyGx1;N8P!;UG#$(oFh>v#S; zrS*X>wKpc`9Xnh*kuMWoHowGwDgQE-v5N^m!6KnIVIu*|*j;I)f?Neq*-!yjY0yW< z9}LJDkXL+XODihg&!HXV&Ri_uD$>oZP%_MrtQf*lkDya!;j2-rk#+niCH79~oo0#5 zVH&qgYEf!ti%^TIXJ6|G@toFozQnfNv~OuG+9}!@E4rll;RU1A87dhZU97s**58h~ z*HkG=1CvD}M1p%IWM?Koa$5DnmBT2en?@9j1xyXy>7@(G%7ubcKiE1rd)zXd6R&BI zxxOoXPsUlq(R{oAR+e`6Es-*n@-rp#%AL}ij=9bRQ*~YUZa135QAwM6zrm}l8ATb>bcIY(k zT%6&Wy|2BcK^H?07y8~El@kemIgG3k5L==cu2@nNajA1?a+t%-&K<}d%#GFT&|KjD z?n?U}=HB6E@pSqKd^3G31z-gfB5omN0t5jH@Z87@h|izd13KEE+Zw(Qy&KacQ=7nn z`FO|g;LC}~grff9#ukt6En^|Yo^!3dlZTTdfL2G~6MtueaA?n1kI?sE95Eb6!Cs-C z0+T`#0dkq!R05#TNwaPYDEmHu5gAogqd z*U?Y0o$7(QBE=#vMAl%W_^Oue%Qa8D*Yl#nqB5vHZ315>mj->G7?`8r8Q^|tzz^XA=9b7FJh*`t2J z{$PFJlEpXBgv<|_(q=8Aio?yWk98kqR+?ysRME_Sn!PaFUo@yFGMSHktucD8)4%#{ zRf>EeVs}Z|UGaKx;Nt6kQ`SY+t>dx-g5$#CcKxYRypFQQ>`3vldeO$fN%~1mgW_JZ zg0`x)7P@h$UT7slZh4bviHXo;&Q9iz_sh_P&88jKn*HRt?)mAKC3DYl=wR5N@Z5yh zuNpmjE~QP2_{@K5FRT=H4DY7z)VCOVYI=U!7d{Rj7V;I!>TVCUpX8XBX31rVVr~zP z=qg*Y?zX&NKIeDED%h6(kWqI9U?pKYUhTdjI!r;~oOS-@p1)0bpL$U}qVi28ZE9gE zusW03*j3zhY1?Z&nP7;3mM8Ji?JVdbKB1z|P;5AS=tWW@TdJeS$;U3=%{>p}?W#lb zrfsW=`0vT%)xqF z>*J2bhuJybKn2Y_7vA@2x4LKD;oEysS5ncb!90#`h<7JW6y^q-OP60yZyDH?Js_MC z(Mnj^777X(>*)g;Dj^9E3W^s>yKm{T zRCyEFGJJA7O$c9z1*Iu^tT2=5`QnHX(m#)hHoa${V1KWpMk*X=3_`*J978^2XpIvu zFXu1VXD=6Q54gafipyEg=G#+c$HR)tx!vJQ=L3~t4OcExAxt2E7z!GJ7YYW$3+n&i z)2P7KOokWL7W!Q06SBCetkyaXi#vggHpmp7X#xqcL$E^hRN@_o*n*T zAHv&S!7UuoFmiA9Eyb9-a!H^rw`u#D_!8*}^ZKFZ`Ju-f|}?3`6w#O z0>xRQ+TEvY{*g56PQJpw7L>Gu>vM1&*Ofzq{5}-tOZ- z*7q>Xo4b{1n%5%0#lBGcMVpT`I~2!myQ(breX$mnHXKs+cLqrlu!KimTL+FUuJd{=N51bqwLIP*u2m21tFx$% z93sDKw3?U?A>?}LzHh4e9rA0p0+)?ou8c&Bda9e@$Gc(c4K@=&wEZqK{!tGV%%i^`c=3;eRd-XBZ4_$K~n1mF5k$6}iq znKwP}cQw0Px+4t})2-NrVEAelEY$5M<;~|y(A}0{N6)7_C8vM60DYr^)P4+-2@vrbuKu7Nz7F`>yu3 zayQX=u8_cGZ8w)+GFYe%@hUh(s7)2s;o94LsoA|wjvnv0PEU{`nz%LTS~S2~k33s4 z`6UFKl!K2({J9zH&&I?ua2uDOSqs&eP8Wx>Si`{(JU=?~t@Kd08xA%L#YO(|a9rY~aVk#>10IhB|b)pWP#raTW)ezTOiD6d9CJs7z) zDvf09E@lmdAYg5}e*Lw<@h}9ux444l7@&EsZq@eec)_*h=9G(2#7YQQZRxfKJhnp# z)nH35Wg~!RkxMyQ3$GmZd^jBqW&z>lu()3AZ3RlX>A2A@tTm0_4j6J{Ij$2XyDq~L z;c-E{6snd|+H}8f87d^F#?oznGwg9uu{OO-f_A%yaP^`J%;0%Dzht4es@OkOLdkR8 zI~qd3k^Y45qnZyJn#}Cf!#s=4?yl?Gm>{>o@i*;BM!PXWof=o|xXfdflFg^dhe1TI z-Y%6oOK$t20@H1W*eG2(NeMA?wktbcDdub|u6ZDcSTds#yu%9!F(eQlu0eCzy#t-5 zTz1}=EFN6QyRvJBG*B)Zt7$%*#IB8A8}QHBR5vR-Xj6Y;hu;OY-1WC~TDyTx>=%BR zZr4sB?xu_*n`@{{@s-?T%&Udiv0D&9jxEJ(H)6?;OQ$BB809Aywnl9DSmOG%**X75 z4z3G#({29d;PIkgZR$yx`vl?rmQc;BmCL4-p$&i7Zu>+-u1YD@nRdp+Q52UNA4zb5 z=DkUV<;A1_twHq-rj}l_0Rwsnw{gd9{ciDh)E$=Qxs1$5cn%)#*WtsD-dB7T~zWU89s`2smWQubM+_*s`i@6V1LaimGg-#_jq< z8+B8N`y_np@bTd=0~Q{QarHV0Y;X%IW#b7&n$LAqW9ehPg=3#RG+&Op z$~aaF;ia=c zI)}b{*zbk!k4tjUG8ZmxbKY-Dh30I1t=uEF+lW@|3i${N3ws>UCZB=gR{Na<$Nd1&Mc_G+lGHr=H1d^*DyL!fkKf99+>-ZiLlKozGKsk zD|ITP;lOH9sr)>}OK85sCr#GTe-&+{`6Y8g0t;~M&}F(ohX=L)XA3PD|AWCNOXq&o zmzFw(H6>0zY0_C*t_Mfqd9K==^!nkN$2}I-2yrQQM@#76&ADJ~*06`14|gRIT@;2s zr6`b8W2z-fELLz@%~pcb=CCs+)DI**7%5tMG`uH(aNPV1r9bRd-<&kI1vSt-{ z9f=Gb$~_0}HkWmgTaYy~3u5AFRb4V$Uf8#A{0_RAs$pI?CUBeW*T4a)PgR|%Hh&oA z{NMtqb@RggH`*Bs&N=WO=j3PD^%u+Asx21l{m|;)PFVt7h#rpof&2OmcC4xIphFuQ zP^}-a=3d0Mwzz}JEeUH~mTp0~JTs4rOZVy->ed`9HD3t_P7+(sR0(+SYH82pXf3DK z6q|PBtu-&|r<>@wS*D0;TAGI~k?wK0%m|6+%k{<1O8vPCWn!#?-K&22`r4(kl{9c$ z%V*K{VIgit%NWF&10qP{AZAgs2;fXuKiwXL>`qz)TCl1;MH1_Fwwo$>&Gl`?<}0Q+ zP}M;kIXDx@8B5N!V*=}Ox;a=za#-opc9o9Xlsj5>(+CunTC#>eXzbVw=K!97#srmW zhniUccOy|#v^)dKDQJA4?W98FHl=DcPWFS^VYkf>K)=Sd1|Qcx*_7uz0kcLmWd`nc z9@?jgnf{PaEDK?moZ)Og9Ga?ky}LTTs`n2SN8445Gr_W{M7wS* z$zi$7u;^cqbT-kPSqPmQxoE!Lue`0*>fvZU^2 z0-N~s!&P%ovACpA?hF3RIF_7;OGPD#9Zck4kNugEmC~{!;{w!Osb9M?|H2Yl$!vMh zxG2(~@gY)X(vBcu?XuGFL)_8!_*04K- zeO!&eC7id}E}Q47P7$I$4mmbs(JGY^duX8uHm|7|-fL~|mxa2U6}l3(-9y*`cFu}j zVS)DglwTu@)V#6B_ZNf?9d5U*=?_#c39W9t4ps5YvMt#x^D&*-ohN5nl-u-x572v;?}(L zXte}+oMjOGgq1r$Ph`2YKbZbP_jQ*K+-`YzXq$T5iH>n_s8SWUU7KcEox)tBN}^$; z*)BZFH=dMLOj%1%LtDA_ zJ{mKg8n>k`4W2CBcTI_Ue%lPA4$wHzY{9?$S+|no$Y5J?!D-!o_58=Onae;gp$hXi zu2p2fMF$57OTNt&HF~3k6(-B=ZM0^CRCdo=ePU8ZlP;gETMjEwQaVqSdu(^ zx9WyGY?p0nQ3fsX$BeDiUcscq?$Iu;R8p(cM-;tVVeLc~tR_frnoXPXlZ&fgzCt@t zE2&-mb~P9(bYQ-mLj))U6V};VaeLTXJR%QUpEtF|om3Jkcp;qimtAfd9~D^#7Jb+U zy@pFJMPsaSB>Z&b5Zkao4!V_UU>*kFht2fj1aaH~s~BK7M0{B9Nwj9KwMSv04q_KP zlN~|M!F;)PdFu=E z>c)?S3lxcuVL#%*`iBSI% zXH^Ne2;QSYx2ZzXxx&Mxb3pd(9W{sffOA5qJ*gR8XpX%vTlU2y!Z_W6GK(dhk7C_% z2f6Ao!q@Q6p*D3g zNCa08*AFO&Lj(i!p846y@yt1{J=Md)?=iMcKr9nI*v0#}`QkGikX+8NGJ3Kk<@nXB z;cJ(d(gn@4G;gkp$RMDlHJ~U5BFrm4HDa&TF89{u?+TX1lehG59n!WKX{6@Iqdn&? zgerGBUhOI(d}a^3Hi{DZ=__WT)DISEmaCs2{jpX+e^^wx}%EmW)2IpOQ!@$;B8 zANverOAnwLp*kw5WpKMy7#p-BnepOD83zWWSZHW(#5+7Idr)?`@I{c!k123xe|4A7 zx`;R@U30g{myJrjD?;DX4x4paiFP9qO7xna(zL1E<0ftF#yI&r@%3)_n(}lL8_~O# zosA6@T3bA!RjDP!B+rLyYgO&FnJ&eK!J#n=%Y~`TVAI{g>Eug(-ijnH+}Zs|mfa*G zw4&*@x@L8al}7t%b~AN{>n~_?UNz|c;}_r9%zlz-xtEUaUG(1T)enlq&X@--?J1{B zg=?v)8r~Vq`WO&?61IHM%CH|!oPSl~vIY;Avv$Tl`*Derm30Qf!)kJ#-$1w5cPqSu z5wFP0ZCs3-AvOjrC|&8|XcTD|4#Gw8sI2JEHzss!=cE!|zerQ;JnqA2(|((&+tD+Q zH-?%TOdZNxTXs^=S6Zig=621}v@LOL)VNmHVe7L##8gPA?bBcK+5f@ zY7D0mH>=;MzwTeqq^vG>7U@x_k6})KYYTiFcA2WsNiGK|5xIG?+-MP8%=7OSp*qEO z$f73u+4pZ~(NI9|`rOjITMqMBo6fJ);Okm?eWVo+kNkG&$A^Q|F`wtnOJX19kCtk! zbsKkLZW%6nHR#jF2YpBm*0>O~d5CM=4-)sK)@wpF-uoA604OF4>NoO=P&WCvCR;#+ zXBz!YiA9UMs-ar0>W7=*W>PXoZX=bLDMQ1kkwQ{Ujv3k=`SLjiq&*oh2M7f1o3_mc zJ_T%+z(dJ->xEgi5FvrF}ctlw&uj4anM7uj|B#ch}nR1R=)r70JayaR$kW3sP5 z-bl_nGg?j|hF0E^v~8UFc%j~I4jTLCVME$KDWGbK<^FPGQO=^pmO@RZUpKn>ccF@L zn=&(Vlq~zM7TV!Whg{|9g{iMB{v<5BHTHcr7eHW+WpXuN>=4VA`pB+>GrPJJ7xKng ztDn6oCp&iEOruauLZIDagZvVv9bFG==3H@0rS*RG0J>SxT-c8fQ`(-(y_SKEPCmB{ zc8QyA3G?h=V*=BV*n`By;yQO*P}9Orre@Q6jH61>09b<4|85R{W51lR|H|HQsyDJ3 zHD3WP5!j*N7t_CnW>YCsC2hb1sO2XxkII~P@voe9l+k3*&u(j;!>fFH&ij~&)ZP_6VCb6fWIz!fMc}Ej)N&dgLzlIfm5BC zhn|s#kuf%Y>7INIxz@XW~1PQ}pe)t#ftWMdtzNvvUxyKMm{t0FDTKzvCI zXCCUm!X0lg1mC8GXXIY|tAYMdfvBAYFIaOz;$^YXFT}I`mDU^ zl2Y=)4`>marxnex&4KG+ApXlER6n z#!z@UN>)-idwsZv{`=d==0%7`mr{Uo5Tj?)ABaWik#5T~c1YynId5Kb&)yFc^kF|W zSM3l8Zpx}JD!d_IWK*p_3;x{pz^^@8q=USVy*=fVsXG0lA&yqe-=eBBVo9Vb$1(a~ zKsw|@<>QC#h}&i->Ix4}scuE;g>31rBIX-;!4+u(1U(6@i@cl^myGs61-{@VlH{fQ z`)`nzejn0|@!w1m6T8pSG|gtti5Ce5=L$e0B#|V~<&_pe>8Bx(6Vp4NtUH^0S5Ps; z4tZ0#2ey0gluIs0>x<@nj*SU}(Uau$vixTTqfKy^6zW?205U&ZG~|JETFIYkvfPw1ch@Mjh|0f5LWbnpj~CEPHf6_? z*=4r6SFVfQJpwdNNkBf{n{u)&F_zoQ0O2IB))@fBuJ;k@SgNWIw{xZa-h2SG_nWc# z^$H(Q<-Ax6oc6P)Cs6~kb7f8^0vs~%4vRzu@`UIBobCn6vE1oN@eYy552AXj#!L+f zXIrf7a)`5=3W(doElk_e_3CqZUrKv~EyjpR5qOEwVJarJ2uw$h7SnB8BFb7m`u@`2b&8Ud~RGkt0Ek;8;}lEh7%Y6zlq0w#KVuTN`y;iQ8>Z4<}P1n*#Q0bQ?&xn>E^vW*{A?+xY#m7sQ= z7spM^yquv(xW2p$nV!Z%t~&VAs<%D#8Mw>mKjt1cRAn8KN%DL>IE%=4M^T|C1hVw@#n7Zdi1F4E@aT;v~odNzqRYlaZg=%6#23f?* z;U({boLhE4Iep?Z0={qbVgLrBxM;!Y@5*2ck$6=jv#>=I9CQj@XUDEqr0UmMdcKcO zjvkhlqn@shYq9!40jMQfPr;$Nenp$uesuvX<0?L{~m77~LnoiE0lY*_Fjg0t_Yc4sOfLE@{I-KIdG zHcrr0=@N6eU0uy*z#QtE;PV(ugRD2w_)qZ6MMf`_d>AqUUGRZBS9i1e0>erJ)HhzUmA1b z^Dq-_L}6S9au@AnEgxPo_nC;TP8=&*a{-&+GVM)8yT0zhdm5Vl&W;KBGd1@tkchtC2$mJ}K-^8tiR^w37Jsrz6Kjd;T zmlGOjX>7baWVt z!%uA4?5xj3Ubd#$UqxO)jK>QHm~ckVJ>`zWvq|x&=m?R-*As|%MnPQnPSUP*sMO7_ zu(iA|*C@pg_jYpTNBG6e*GMM2SVc662sS6{ zY1f22ku)JTl9IJ{SoW52Ufnlvt_*^K51UY-F2n3Ys zEG5{ThSRtTOT}ZYBe;5VBZMMst^5{!8fmCjH$`W+?Moh~t1f*wRI3n|7gWrzKXW{p ztpkya)PbKW14N_#7LV||<_pqgD#bE%-wO{f-2!^(e%ANv==lk(O$tg=7rtSQm%bNj zw~H2QG-~?!aKE^ax7W*ifgJ!^CcC#fYcCx8le>MrN{}p(L5i0WAe|uY*6#Q0Y%>DJfC%YYTC`#tcft z2;or#$cY&Pr3s%-{EUQNLhxP%ADV^S&nsDPI@fF*=`;7YQ6S7kVt{_o2Qy1A9I6-n zjCYUuY8Bt)nEM#CA-0mlBSC~L#>j>Dj%R>lriAkrvI_0V)hy0mBU8n^7E39h)!e@% znf`{rl;^kp(vzOf8KXpDV@DO3q`FJvYJvuNS*<$&W4mJrijO+;&GU|7+ue9i4v$QM z`Vadk4e7$qGTyFH{&*bD>V4cH$!agJSQAo?6;WO2(kB1~QSh!IB2LR_oP$2+vtcccy<|yB*zJ=I&UEO0=8UTxgn9V|1ug>=Mww2`)#MN_- zn9@*BhMT0&qMV&%c==MY+y%8!Qoc*1I?1NS^V3_gsVur2DD@pZ(FNK&3{;|BxBBZ~rnXPkVop7iBKJ68GjeJbn1#5?7fZn4; z%$?ucj`g;B&u1jyV~o~1d(Mek_+QB6!tdpr+_W+SmfJ(wy{={6vi94rpsyd)umK9| zIVxSA_!X*pxd(;Xni8!7+^jvJNo>z#7T))n-^1vQJriV|cmV(md-Q2pKdZCtfyY3LNkg zl~((fE1yADB7r0>^$GY3_kN53IL&pX38u#VB+o#b3RZ3aQ^3~3;iPX}o;#3NaA%Vet3;iB9$-`y#IsUw>Z@!K9C5)0DgCMFRo|^ zjK+oT)$@2wnxuXD1*FcMQf~bwe01!3(5e$Ny^|k80 z3FDVvWW3-YE5&r)uAUz^hVi}l z9$AA7U?}&6@2bKS(X;Oaaa`&@YJV20_Zo=MJ!rC}VJ_R6D&FcS;DM)f`g}kC8XMu2 z1Z4NqD-)q$?}zsG5Xpg9dpF6v9A<^l+VC%+PK<=8mQHUTXVR8yls%60ci%yFzU@z4 z_|-H0&;oF-;!Uw#y195FbW4^O=8p03WhHAjAz`W#A#Octr+q=a&Y*_tZ@iGf@lefT zHqm^I-mh0Pw6ZWK6d(3GoUp%ov3EKj;PF##qeU7M-eZNJMt#Cz$;19|s~9l{-ZM6a zr$ye6olFr-g@Pmv<&oYK|Ad0sASKSwFAIu+ZOfhZpd{+r4t@30!}c}2=U{9RI^e9O zbdF)fqof>cF8{y)sYafg>j=Nry&zs5?djL+gn%1Mp2i1i=^6(4Hb3#Vu%L)D0kiFQ zrwq}gnwZNA3GXaze4aW;?{ClJC`fIRQTh@&*|(d7sO1&8V;yI)hF`-|-t5myBh8c^IUORRt+%f`#5sC2fO{xFjyvN+c&h4G z2v-uqIz98-(fX>kI)rvy+K@CuTj+)P6-*|dT6awgA_+v$HFt=G2IAJw!D6j3=#Gi{r~TE13=M>CFt~(}31)*QTrG|V-XD^+J0V<=YLX-5S{Q_B$$~at>grAz`zd@X^uJ- z?<>3Ae&QI;fUJXMBP`qRJ~@VN1NuuZ!b)aE4KFF&76l=pz~4;vRN)~>u!Zm48E>w* zRXOQBA8sWx%#iE+F%YXfn8XwR=^@)bIl;4O^XQ^9uCZqOvI(Tw90N?+yBAv4`*~1L z$@i4_b}&1oxPY}c+dck`)MBIk>V7AI;I6ORLkv+hDEgJqmf3TzT%I5b2{j`Ji>eet zDZ5F_9Xi=^ar8u1)zAM7%dHD~v6avMkmTrK=wq|)ax->ZMd3%%n6j0IyDRGq;3@XM zvab(7mmuyW{Ab&bFohbUx@o;yNJFLmQD+l`l$r(WdUh6)D8uGe`A(KJyYQFfWBnb7 zVp0XO`ETQprc{SAe^+>nEgI;l=mP=|pj3l7Yp<8ec!S<3BN#;&6qVvfzbZBr_Oj|S|4`mdUS@4BJOeK;#O*M*>=h=WC=kA3g z<+CK!hBX1RUw-l8%|fh9Fx{=|4`OtrxG-iA?of1sRG6T6-K^vQTn7);BHh&m71Lu` z=ik5Ix&`=>X2pnad~p_zTE7uAYPpurO3U;in0_8VMxLKt8u9p~JkTSfEW>BZK6Gry z8~6m>&wfESM8&BjSysGCVUN&AojZ+JPOV3!PsJ&j!B-fMYD6KPe*?HWntEfEKvxxq z)E%puhXn)avpeFGX`9rC##@F zRYM@#{JS6WZ+k)`*on8Im9<@oqLelf((bW(zZAeuJvYdtm&-75l$S44HWBQNBAWse ziJ*h0!o^wts8p?=5PP72NBN}8oe|4*LqWXg@Ej6B?Lw|_ewwSX2+MC5G*G_`BWYTg z4+qfv*-Jdk3pFH~VRKxvJTZzwvK&!~=j=+mTd+xT1tsw_tF<=%Fm~uV#(!J=eqyj)yFX6!=DrdK^oO-0e&|y|%Mxks&GFKgB4?9Q9Df|t*cE+lWkDM(C@cMs z_m~g{A~4ID@JI=Rr;b0%7ad5JJ7}Ew3|Zrs87lHmU={c(br#$U3r$JB%@SP*wlyX1 z6ej;CTwaj;!0f7PQyK@!ieDJF{CGVHRAh=^!6md_xS8g|a#7`DNLlOfr`?|lASy@fHFy**M_T~+a%RMS6zMNUSq40n%&f7T{4 z<<|tEs!^qMSk&b^1fjyFo}UmW$MBiY{nA7F?N=Ar(;kT}Ag%0gR*?$eeBGenw!$L+xlmc()OS%t@x66R_|Ta8Ix39C8Ey$c`J-99&bJ4cCCe3 zkty3<+=~{y*;rnC^+>#`Dr-M^cU(&Q;(lz>B-M&X#nToNALS z2VXR(;e&bP?YY-)Oj_QqHGT_o%V^HGolN$G5ZNzXGH`#E*+AN@*$pI(SZ=&?Rk<(9 zbuq8I2jqt7KfBA#N)Er@U8smq+dZOy0Z%P3eMk8teZG15#3EwS5+RJK;G@Erh{8F_ zP$}>`MF3$;&IvaJ>o25Lg`awwUlmalBaVdMG4dl`77sP4r>_Zf*7GV3Wkg{8v5%O1 z4vzet_cQQRWc>iJHVUJJme8#g7Cdb3q_4EDRRwQuD*K_FmUpc4O-tNTvvnwxX+Ub& zRVeS+pCpk>^ddls?OT@M=(UiI7CR&>aFMqFsM@IP!(;nWH(IyWRn3_rSN$sNPXt;B zMq|@TT5z?jQ|Syo?hZ5ka|uEQ`=0Pl1(i^Yvi=6Tk9<2W6nLso?yKD&W94#qE1bqC zB8e&|>jB}cW^*ul!B+dM*Z=@&%&i{CXK_T^NSf;9dk!B3^_8#p13l zIe&!1%k$4k`txcJurb~j`ZPR4Mf&5+eZTn-ghHMpILT@Ft*2>Cn6{`_C8@_G#5n7z zE-1z~Ts)3 zGBNK$fEJ7ZR)}_N@`|~c2MEX`^!>vSAaTn;PRwt?>j(CJGS6uxL{#M%7w-XVGbhFJSAI@cG#AgQ(oL0pwQ6KsNtMqhI$muLWpTg zRJhx}+lU}I0p|R6!#l&O-|v@{5A6wEM?1#BO$@W`jLU~)Aci(KmO*ljXig~QKX3G- z5s=-Rt4Rubk=+?k6}k6Po)Y5!$`59mQ{9U9EEFgXXM9Nzq=9Bv{*wey-uW1DRQwLM zIs-0I4*^04@^uSIdeNFzeuM7c#8#!2UFNs#6+%8j2(ZwM-oL(vXqH5A5qu3ac;^hI zTs0mtlF`fK z$Jlh&F^Sb@qWSfdB+FBT2ur9l+!*Zr>7qEK)5}*U0wcPAG0_(nkjJ+0#5KQI^8Xl-UW~lA_d!joKZ0 zR~;i25Tp!?y-k8hn14cd3+UY`;$WyV(oLi^cFY@!9Od9Q*hwX?587wnYHJg-zOaD= zUo4{t@;`|UIq;850OvD7za!97vIv1UW1m{>#_!2JT$i&>JY8$5+4lKV38kQ#|8 z9O3=%WHs@2XDF8^@zNdC*~trkX(-9Qa5}gxYNmS{}Xpm-u2IoSgO{85Mko! ze7GI_COuXFu za&2C}Vth9Vk+`GI9_Fa_@9V)C2(Qxu6)$624NLH3-bYyD)ER0T$>aNBOFG$kBYrNmbcaaH0>h9V+Y`nUhX;XW$2X_N*${MCMrp8p^(-}^k-!Qh)=;;EPP7gG zd~s=9-nE_z+4`?|A|81MlagNkxy#DR=zQTkm|8qXvfS8k=I5f^$LnGl4d!)6M{A;? zBK6kS)cc}3t}CvWY@-rVjtIyT9aq}mPD zssFc$P)L8UZ9j~en*EsHijirN0@J<3p*ru#*!_kZTDg0cJ5>6urpK82mHdJO%@Cv` zUhOCOcUvexJq59kYZ-**1s?=XQoNU##qtEr-f_BQUJl8jP|5SV+xIP_nC z7y}PMA3RMO-Mj2g;+Ed7x-s)Y^paCCcL^|h&M>93E4PPR$#kw%G(30;8wKrezl4!P z12ga`h*$O{4JUD0t{uXg6{*5H2aj}nQbNX{u| zWh_xxdkrc)5}4fRpIchR5?GWAwOn-BEmFj-a*KLG3SJq=eLSwf^C(!zr4|`9@Pv9s z-uJEAIkG(!ocp=2I4TL0cOa$_lg)=R>^k7n0$&8DPZ}AWqp(CS8g@f<;MjZv9*&do zEi~DOH|!pFm!p*pDH;jonwJ}CMu&GK2gr|FcDn%0r#Gl1^pFJyKAR`}CjVCcn* zi3qWg40Y5$0^#pT0|JcJxI?JtGJo&$zhnMiS+jNhHO@DN-(sjMOby6JtkSrnzUMZF z^PX*V+tcO}Gd%SEMsc*8`zi$!?7lkD8<}wndMw=1M)%aSYnYcZ?yR%8ls>C^=lMS2 zk_CBnHyHWVlPcxKtNrAC1x9R?r!P!lG;i8lgbv+rH;#YKOT<6ty7her#2oFq>+*Ez z4>;Y%a%|~N6+FD3x!BV9@c5eBoF8<#aH`?y_dXTw=X*#{d&6L9B}jPt@{c}6PUY*P zyiiU2Sqz?CKJ?(?**k9qnU`Q3&g-$QdY=}vuO+Xplq=1rr!y$I_LWDa&>vg2f@w4t z0{VYrKvXCs+OPEl10iYp66@S7Yx&bp~p5JAWxX0+f&hvO|>Neo7!Jm`B6@7o%ql$(USM+P1pg)2D>* zc}`xrNX2g(RGjXylsvlJ(U0_bS0`2m{BCycNxj}L@m#E z^rTHXw6wTLpXIYOS51=tE;qa)5Lk@;B#jLbmvUL4LrXPp^Gwru>euA; zu_R28=a}$nCBD*Ji#B!e5P-YiCN*)})+Hi*_u=~f@WGMsbl^6qcIY?P%B6sQrS%Wi z$_03#p6#|gO!m-BS2C=tPg1JalNBE`DSG67WXkz1E7CvBR+S-G6FeC%6>+9+a$Z11 zwRv%q2`rWDro#CQeA&`ar_MO^Tkrh|F^{774>`#FQ!XJi@U$kCfxN!7w~k3&HgrbQ zYIH4Q33QdXHMieEpf`T}G=KmJd=pHqA-Ae^5p&4rNYAhKrklrQia>Y85;E&yX43?B z`mB3KmO{!&`Wznx$ev=#mjp6auKkIR?{AES{Ow!N^??)SQ!)BaG4LgUgd(1dG{%2n zxex@)IW#)|Xhy%Y`KjIsVq*jTUcLx%&S3~{+24FY8V_Q3U(9FE{;L!?Lek}5W%T#{ z*nW^C{NFMEL9`Wlkx})*ZPUL_^;yDd?pW?Rm>OsB?a(?B8u-=yO6;@ zJ=z8!$+i)tpFe^BtYqeiZgC0ga|*2#@AtW^b_S-t0V!6sXjoee+zv$l`CkCWxFt|v zPaDYaSyKtr#t($Q!WW~*uhlpw+2Ltwte$U$!WT=VcF>jUtJ{ZJBGFW-kkFkQm1q+K zW^&TM<`QTwWBjSv2D>H&4mQdIqxk9F#yw*8_vl2UVm zn)JVzG!#q|iIt#ECXpomnvne+q3=6#Fz3gXnc1wM>$dgFKC+PDM{^`aYDU4c7oIKc zNI#P|ugv~FvR5kr(P+Z0IOu>DiBJGNfq}lh66Ag z)#3SVKnX{RGlGGE@)csC^f_Mc&qv1*50K0OU}W}$kO)N=e8Ey>w1{~{?7;W zNZ!f}rPF<0+%Hi~fFPs-d#Mhk>8dOw?M#n`VhM9e*~?1C%}A$|ax6mZkgxDx*XLek zHEECpE#GRdgtd#Rr-G8uTmB0bOko(3aNq1n3x|F?oU$DDOpW;RL0RxT0b{tke#&M- zZCdcdX7ajRRt2p{KRTS)=j$u0gfXRJ45>AdRp*d5$Huz4?`yb8qo%Qq#%OG#v8~3o z?WD17HMVWr&WUZC&*_EV|GK}y^WwbQd!M!UT5DpAG3VOHZJh7rCyKDij(G`-&w~B9 zWa%qVEK2(75(^BvDX!h z^@!M(FQ&Y@O_<}ayeq=@dLt{_ag~iD(0G3;tak5a4w$iQsh4_g%epgBEaLTJo|QH# zpja3Ci5v)ezS8MHu055{9N_#nwjUAhiYzGgr_aRb5HY;0P!Jkm8bS507QpDr-IMij zola@U0**3#{}dp07yK_X^_RD`MZP?|0R@j@35qekuncy$e5?px&qJ{{*u}GRV{vh` z5>QZxAJB|3FxC7*W#hnCL6JG~0pR~gq_!8q58h-9vV@NO7VE+#q z<-=zUC7Hy(VrKaWo52>=K;;*%&Wi!&?<#uLf;){L<4_syN>tys9|Peqt464gy8*FPnH(poDXQrJJ8x& zyo$}_rhp3hD|z_aC&q{Qzzn;xW5$l-8*z|*mB+-ov9;|tD;7w`t5YI#fATGw#w#*1 z>JDT2kEt0E(J2w}M7ZcD=4TxU_ zi2D=Rh*);Q)xq={S2%CwI}#oq7v)CSyf#Oyz{Eh?=x$Ixkl3q;qVlE%r0k*LO*oOd za%cx>LOYS44%X!3U1+yfA9zZ9(gd1;Z!y@1!#<@YL3@g_16ur(_#qV z&))cEOPQYmO0G+;+_K|IFb(+zx;tJ`JwD&0G0{@+|1-%?$f7U}i1?VX-3^NQ!)rQm z(=F<*L5N#-l!AB)lIngJjr3V4Bk*eO$fP2#1HTP*Yq^h(q|Frri71wFuGkGZi2!9{ ze`5ZBg177xzGU%gTElc#Vh;)YLiwfXd3aQo-Luyt+QfTD1gHW6rZn5jRphbbpl;#cj|GHWy5pK@djmw#_Q-B#;XUZ1I`R=7Vid@s>dWB((P`v$6U>!b zzRgfs>}HUVOblf>b=WuF5s?2ql7j?cAy5WeO$*o1ABns#$X78j09v;{QIm4W=zU`RS z3f$ZVp|+4z?!f}$&v#9u_DjOM4*_{p0gU&dW@BV1#-$#RW3n%TUcI)lRvS_KML)4q z9WJv+TvNoAiy#|1;(mYn->mR~^{iZuM}`2HL(>2&m$>4;pQs9{INN9+Ou$+9$9p>< z1fjjiuSMVdRtY0Ax0vXE<{HOOGlp}Arnij*MoB1a9FZps6L+*fk~mUMh>E&qLEXlA762Cr1`$MldO_ z)=nvN6DqEiQ3z%Kp>YgK_P=2PwG_YVw;o0jpo!8lYo;cFPbpDgpv1;uaXu-K$FMYH z*#8c7)T0;BoY87!mVqc=Imel|e(^zh&mlg9{a?U#EYUSECLSOd zLZ~mePIUeLNrrGC@IsbJL23VzV7r*_Gw!-dkx8teT7;{89ZUn2YS61uo+QJie|90v zGXKIJeD@v2N+@^0j2iU6?w=1I%Ejzgc>Fjti-&%%P~fgtz#gTvF7UAk5x6)Ht#~W= zPHP-(l{V9g@7;NL`6))TU~>M~af|s5Bhm%=D{a5>M>^#Q-#TsiYAEKMk;TCidj53; zQ^=rN*j^!iDmh<>qVV|FNI#dEnD;s>>dG9*K1DV0Ff`mB6=347sZNQa((UWllngQq z_q7;uQzdK&h%>(m?Tn(pk>1&8T5O{4hYb|h*!`{N1BL(rU&iA3YlMYGEpABzLly(I zb2%od&D)itNT9oQRx8}%2d3+B?S~DfuCF>Y(`(Wf{TDpu`xP22z%G=VC8~eRe(lKg ztCiIikw;$~)D_L4ekcB3(ogZP>428%$~|LXX`le}UxWx2*-I+gG}Nlrx4X7M!dBvF zsMV8OWV;X-DKQu$L&a}c=$uFxhR;nn?Gcv|x${Q3y@=jb4o%2ZR|@_gm4OoG!)x66 zlQ2^-69Xp`tEzd-epn&+%-{$T1zX-r^h%_ZvQ}v-xzQrEvqu9wXyy_&&@+Ta9skW+ z`O%tAaX%{{^dGZEDXbF-6LJTwtL|TD=F(@Af2_7}1Uvri`Z~y6Nd#M3$$o>zfK_wZ^IAuMF0_1wP&&s=%L@IvV3ICY!_zWM|JR#^-B%)#AieBje z@$tPoXd;GB@U31eFTFo%M`_xW_bRLC@l{mrG!hPr?&c!%5|Z%}hGyW?5^khS^9$4e z)mG@}fwi%`QU(Z8^bQeSpCnl9h5wv4uNH9GsZwyH2F#@3N@y_P)=9QsJQlKI?`F(EZw$GsE-(?o2ZXxo~of z#*$jt&Ik^*{FqflGlXLeL;dxH2Aa~yBi8Q;uPbKPysgdBAGJjBsmOmDM&DHAOY#l* z0Oeed8>+E^yY(_1q7HfcGLbkn&G4sRzDbg6#+ZPr#nqeU+-i!a`st3kpUsV?BuS|K ze?-DSYkm962R5Ug!6A|uqC8>+Xf29)!{KuS`%00| zL=Dd7Lm)UnmDWXbpPH!^>nY#%i`(>acmEj=+!tc=Uh5<|{oVnHHh+;l%iVZa%SX5` zKkN;r@EqFP7o`lfliFt82CvLa##+}|Y!)^4LsnIs!OPoX+Ck?bz1RVK36wR}{M7#ogmr8XS0Ox!)bMAQ4_ zx3HYd8ooQO=#mX6SEzT(mS|526FI))-D)pP|Be=>{w||RyYvqZC-ki=eR*q!E-E5q zdb^mpG*G)@v9bmGCZx_dte5}f*8_)2X|eJb|4l}Frn^pCRW2^@;$t4hFQ88Ud?`Id zDD&RI(JK<%I?iM93fK+8XVa38ieCXD`rOZ|C$Rb*gkGHd@Zf8g!HzrP?Qq-kWqIgZ zql0+G4~lu^dJL6Xc<&16228X2Wvb55DDi0!WxVZg6bH(mFnW}sTY z#QmE*mpGG$V8^j?M61+wI(Haa--|e-s}G(X8XG0|e3bS1`$l$8Rinpqr#cx+!t3LL zX!$10-wp^^?Q^!@Tp{N)W{(PnsV{-r6G{SbD50TBbjY*qwz=!b#Wq6{a%_y6UkxOFe)1E{51bp&IJ_an*2EZ=9mdc<4|<2a#X|aKyK?{ za@Eu!)}M!GvycLUl4(A9UT!0)kA1n*D;JCTDaE7pJSb_kcb$eJ*Oy&8M;?$UG|0)O z^?`zW|9D+Xs9*It@L->rk3`k5-H7SD_ZYcOT@<~Bhse|EK`^z)Qt&lVU+>McJ`U=5 z%OLxjb44oGlo}@=HWG9gr>w0=vY%G9w$ z*JC)F&F^X+dV1J?hA8|gj%@|^X=_SB%%bf1W6HW~DG>P*1;_i>cN^%yx9ExQ@^dXO zxG;tNw13&{A}(J#)_GjrLzH%Bk!IQYLVb%I(C-C8o9T;w3ePA59;pLGgYQ{C@!#>s zE$5c*I@mZO%fQ`4y9I*Z<_$FVQ7Pwt?;zy%bGc4192#z{uARR!&zA({%CgmiN=Y6xT*;<*&Wr@yy)02fO_+gS^!b znK)CH6ix%|gjNPGdt9xQL|g5=>Xzh(c72}4cy0Pag8=bIV!Hl;7C+89o#2+-9f`1v z(NX)9Hv|K(!SUJ~jSy2%mhJIL_h1JhoycTd$tB#m4EvUr){YqCU;tKtOG5Up_d~E( zC2DNSUBAHiUo)L831-y1BTu%#mE}-R*$vFwB(7oTSK}}~4|1umJeBvsfaQf@+<*u_ zxSwoVIaImZwDjV)V9P>%^j>{#_Od?kcKylK{5SVmLz*pZ=F3S5?yOF@grHj1<~qI_ z@TqM#BYaA5zvvBQEtHoZ{dGo;et(u}z|kU?>CMUHsY$;0O6u90M+AnI3M>o#L;FI7 zPGX^~vf4`4@y{y@5g;t+@MK3)VqBhAv9x@q5&#)hhN#vkLfEH*FMg$-WCtPC8nODArz(Bm>GT=cTv?+_70X0jl?A2^evrY{Z#&5Zic z$S)Nmyl=EWSSRk|;wzm0#X9@K0XNK6FV4!;mor(&I=%;e7pG6vQYf`+c?gX4avP+0 zg{?qJd4r^i{hc4cOm=V6mi@+wq!-2IYQaSo6Pn*kZ&Estr6K!vW6dW^gXEu;1muZi@`cf-%ZXk@!@D;5;EC4JSAE}_ynV#}ci<0AVZ_I`Yq_Wbvjn?uW$3$i6tm~eVX*fATiq4+?N$Q6K(*+*coB&X&#;mH)gEc&6+vW z_s75j1;`%(4UHWo{C}+{haUuGu$?lvB3ye!N3y`+U^P;nKwbo_&%R?RQ02tk0U3Y) zDEOyy%pam;-q1n@9*~XDh-$R9_x1xn!)L>%40VHuVj#%TD#A)XWn+5T_K((w?+mC? zwr!Ti6$V-kdItA0xa07o5JwD0!0%mK;#2ya=yX^~Y=QM4`%`xbK8vKnTi057JbcaAg{AU8%KH3o*%Lax0xn8;9A7#o?x^f>)ZJ;v_hTfzMZ>-e z0?8fB9x0OVxiboaJ!z!0#UliKp)!?lXITUj8Yn$Fd~VM~{-EukQXGFK30fNX(%!PT z0(6_)2lM8=jK-XtTg;R6_pd4q;Xv}h7{;`&}#NW`9LTH>p3c`XV_(|%E zdm}r{(wroG5e<-;#06!X3;qZ03N3@pic6o*p{b^ib0`BOe79G(*Tb)gnSf92d;nI- zctC;OhrCy+R6K6&`twNUb46D@2qwUK6v*%QX8rsVDuieG2>TuscdwTqz=&egyQ6Y3 z3_v6Ahyd;~^g4v?Y{=XM?5-di=nPs4*sies_>bQD6o>+>zKDK6%AZZctxbNmY(yYK z&_31pZbz4QLv2R~Qn3pCC(B+0D_BugF#)C`VTCTY9E>yAK+z;rmcd4Ap?gXQ58`Kz3jEkbW=pqJpQ!7direUS=hz5+JvC3B~9f(!!7QkR}c@$_io<<@CqJld1Nv0n z^g!BPF_t$DEX(d+DQ}c0Pk(~yA~2As`w`LAqG&C%Qy*A&B?SNKPx6V>%3?fTO?FE~ z-?k3bKQ;gQrZWbK)W0G@Lv{P1mxc7PVWE74@#VZTxGnF{r|0|!O^zm@Le8TDeaibu z78Ll2e}Jv96q)HfL5bRenX+3-b8uJYpFvVL`28WWllkQPZQTnH)=glKkRp>Qc;2`q zA!1p?c#eGY-n^g`nlVI=3vGsg94$E$Nayj-T!hJ1>F~bX=d=1PW3-mtJF4_fJNEdY zp#dm(HSl?mw+|R~>2>tFU`N^ooZ)@hZ@HiSd@IGOnR*nsR3I?bq@k^DSc;PELzNW8 z|D?IuGGJ}nAy@;5=DjC4(xRTR!x+mek8I>mOMF~KO%5jdpH2=8hoSU4vOo{1t$A^R zig5>3!AlrT*avGWdj$eAtX}>QM#a>Uv}0-n20%*hHtSE6T~S3lPww`9h}OPtFx>Dy zT4?x%%UY2EdKT1)3TYNpJ(hApEc|JENH09rB|q)EnERor`TG>E2K|?@#Po&26#;k) z9C_8^?HN)u1*3SN)c%mrcM6KdyGcLcIveqn@KB2^KN32qT}yDBkb5{H<_U-Gf%5CH zxuf3H&EBpS|2Rzs9eF+Pkfi%$^~Ndt!?K~o$RD$DO@^Q)b1us4;w8nN*L5=dx;JeZ znYbYw@Hk%+(QAcF;PnCIwI8D1T!3fAtoZnCC_Z~zwa%n)tk*Ts;)4!rj9OAOt2w!& zKq!JLQ;vwh?1F+o8SoLX!Qm?Uqhb54xX*zO=D_F}$L}%?n;U=Qm8sk!V#V~Gpi~Qg zDjPCNs|fFHj@4|#@VS492ZUSL6W`$>CR4bU%W1e0o3gGw;9jvCED`k(11HBoCVM=W5ezIzg- zivtgg(UavU3r_kCIs`j#ac3af^zNzjwx(gx@H9+gb_(>%8}Ce}4-!c*klvTrghQi= z3uK@}=y%vFgGZPSs;)<(DzasujD9AJ&B;i6GvZ)5z4LCC(QQ6AhE^g&2>CNQaRM!D zEm*}HDxrnINfdXd5Q#(yR4%_=Y2!4z-?hVV&ifSlW5$teQ81^CHH1%Dw;E`)7+uOd zpvJ0B&{wy`KQgso^x8{$9>7P)WW-^7?T|0c1(^BD(>+zOJKBC{lt~*|&U7#7T;j+q zNYhku?Ziq%O4d>|!w6oHjP9f6KYqa2jMP8$E`JNinOY}(!CiUz>@igT^E$5%fgIJ> zY1F0~n27c*)l_*UbaEfba#42X8B$9pMgm0!M?YB1d@WBYmN3_>CjwDYNIXSIlpn+F ziA6dc#bmnKi#`COiq}U0wA|o-x*rGvofbX^Le$FhXUsBKZ7jq{by=B&l_U&E5iV`7 zxuu`fs360MF;DVh7N;?AgCiQ$nO@$)x3Wt&9qd;(>b3n8tBuY=|T822L$m06ecS-R-(hMOq@T=RVa%iiG`79|+-Bzh#diFrns3Jb8jUSfhNf=@_|8Daprx zSJ&2FX~S%;%{;th*_^TX*4eJf=Q69zUtV=Z^Ym;f8D0O;_(1tsJ{AwHoI(l28ZDv1 z@0*WOEYG(bp1!z5mLF)g)IQ(EJVC>^-z9pYYnH0^fCFz@JF&m&sfOSKuj znFTBzP?#=89XEDf`CTY5e)kdt_GqzaJHqdnU%Au@j3#?7foc)rR(<;o9-o>ne=srR zA!U%EID1p}KnC%qE0a^Ea%Zlh(W$Flh5%v4rHb5{q1#p9)WR1)CD}Eq`5ivO{aWVS z^(w7BRxIE|Y90wR_6{_ls*LN+i>9aw!5M7}@;Ml?~W38X&7l`v@^k4HRJ$EmH4_>wi6#n5fTv4xAC zdGkJpkEv*nm75*z1z0Zi6V=ar_D26zjtACL;sELYTHok9S>kXP&09~+?t2+>NcRny zb^96e-g+UBw%MkYy2DX74iwilX|n_kSHHsy>8ye$_QTK&(k1%>PD4pA=%W7=%9Fol zBcGdvUnx(Ln^LeVl8y=$F8^JZ`&*|LbGkx`x_pfE7s;g4HB~@#BOdkXt?;(eANk## z!HQ+X90Ze0ecHMuRv#_5?K`7LT5 z=nf&B#x*ZN#$_F4`o@4@mEOZ&imIKm$mg)y{I5Z=4O>33DclozP35a#1>*4PPt%s& zQ5CCx`L}!DbSCfeq1{)>$+|0L8@HOZX!!42>S8}Uj`%Sgmve8gA`Y2%kqUz7wFPnv zRSallu%9Dytr%-YL*v7w362<5w<-?(egh8*+N|GNEFt9ijS&7w#L*~RZ}5FfJf!7i zL=-+zjJBt3sa)I_e!wBBI@TFDIQcT<;&@)1M&=`tRU_&QQN2}-FQAMT5~Aqp%jk?$ za)Oi~zFtU|Cav$?4OC!)BVAB(I{O}SdIjS^3LHUiHK`XMV%pq zDxxa)(4ey{=EYwusm|uFl__wbokH##7I-N=TNn1`wC?<^8K*|S zbx^gESqX}w2317S)vhVJ)M2O();J0DR{bN~+StiN1=Ip9!+^(|0nY@Mi~;}&B7y6= z#U%a6M&k}xnk!@@YZ=i=H%aYxJeq|?dNb8fl+z~GO>_zD^l09zpPci}@{6d9GGxZ| z;VXyGZn>0(`cEiec>MXxJubrK;mN?%gs8K!qYb>=u>`^611=OtZo6e#MB7?%rMzPE zer`EzRFF#o$wpes^7Onp$jjD66UUNayI7*)@gKD`ocy^ZZayQ`+5)lV1Zn<67r!;4 z3Jo(XT|mOaHvh;EXD9Pc)da)6_7Sl*EmUZ8;2{T3ecF49g2OA9cQgJWbBR%HlE1WI zFO*)kkKFwUix^=Q$ei*gsvBHj9dpwtC9KgXfAwPoI?&FNwScH@G@YGh&I|b2J9^&z z_F%@}`;t($Oa@SQG*23EKWOdV;D#7fqLTTXki^&8te=+5 zsjQW=FMFM02CRh0(tStbNWwz}n1c`VIYhZi5aJ+BFMh0=;( zw%R}zxVRuIe>71NPv0PSfO!XPv*i4P?|G;42R!3uuk}%qi0U%I6s)hV7w1eqozy5? zZ6>;<=1t3v1p-EdkII!sIYOtc^zD98xF_u)op~L0GlZ|z7l-S3j0Fu`@pyWNRLKmxz7*kZ|mvlwYjDEP{2nsgB(q5|A-O*%q_Jto^kM7`S?JIIxm2= zi%0Tec$s^Ltv{xJC5Lk0>tcxq6kQTvtdPN5eLvjw>er1f{VeMkvAq}|SmM!2)(6ze zR27P^^(IXnm(cH2HWnJbiMAkP(O+0gcma;e;vg(vo9&I~zerl;Xg8{NChI9UP8)0R z9!((y3^;tj4RRV)AnV9krLU<$<Zqv*BxTNTFP@J5Ch7KA?JM(Zpxr zu9w#-8f*IU_|>I4rsA!{ffCO$dfP(Igh?lK=Sgc672Z(1ce{xQbSD1}+EkDS2e8@F zl=~wX4442GdA5|P4LU?pu;kM1$Qy&@iO`xYlwShe7S$;G{JQ8sXUjREA^mAgtv`GF z>y3*I&aq>Ec)8s6AQwhdyAd-u2@1aj6yMM*n`F)H`9LzCfleT~kIovSGZ7(l^B(f z`K*o)w?Vh^nnweao~t&&XHpG$@*<#EMJ~oBgKfs4Qn>Zmg^mq5^_=AjmXVDQOe#pF zRKCkjkqMEwQb)^^3WGV5ZDLn6B~*DUivS3>ulZ0TTkY88{h;$93KnOspgI|DX?ze* z$k3mU7D`g-rT(Yx204dwX@z^SL<1J0jlaV@x%gJI>W3x~o+2|osFuRJ>eu!g(a#Vb zJPbveG&ALz3+(l#lGVi-w~XbNMS<;74ofAUXtP zBkIv}<5UZ;#+*W;St_qir7Lrvkd;n>OM2lx-s??Z5aY5vP|}SE>hQbFD#fnVKYUkLIU&(W%seDh!i zlWA<{@25`G^A+9wECtQZDcx1c-^F7aEq4nVl@r8Qu_WqtYpq!}yjst<--m(d?}z7G zNBQA5z7J{fqOs>fbuOW;)VO+zLBwZgd1;cF&f!=9b-nGw>^o0McLK-z^l)lC@MuF$ zic-JB4P@dt$P8+w7owxpD91Z}Xs02XH5RC6zSLR7}PCB?y&fbob zzS&nwWB}%o*UUvoTW=0G#2SDJ@`aFs~ zfv+`5?HsQLS*X5_au#F82_BfULJxNFSkJ7WEWn?b6WLTJqQ!J4qG%1L?|Xz!*n0$D z?0AUn@jAsa@Bobek^g_m)yn>$Xi~}lEzKl|Mg#^4;a#!Re^U&9Yd~s9edx28b!Yxz z$Nr*F`Go(VJ)jabe*vn0bQr*Ehf;yT*}^X|{Qr};d;-M;#tM_bbEE#HBmZ1WU=9QW z)sRU&|GMoT4%Gm7Rud0V74g4y+5bNsq>i2NQDo|AN99FGgQlm@?fA)q@SxY-=;3Ya z8n@e~;6zv^!)5xPQWU*8_u8h%QSDbQb?2v&L%%SZ$o;o*Kk6ESap37he@Ypi#|kS# zIy`&trZ2pm?HwlF*EF6V7N@sKCZnW3;#v>1X4>7IM6%kBLk~leXhKF~+(CxuSpP1z zc}tBFYOPqq!+kyX9(;O#N_hyub9x7#MnLcO;-b}XbY(iLC=8lC zr4rp-s`N(HyqSg<(#Bjt#eIHv&IPP_iQgX7o+E2MHjxRVYqpVdINH6Fb{D{&0FQh* z08C@A->*X>6^fgJ^1e^Q?{u7xYOkWcy?T3<+*_GBY`kQla-WNZ<=)`A>T;M_4W7_^ zyfm%3-+$Fk)W}pbh<_P+9pt&qJX{XL!}War_*ovfdtXg>_p_%&&OY7w zal&gXHz_y!dS-2{=vomP(JDt|SA7fi{+>S>rd4f0fc5+U)Hwokb;6I?-VfU6sn!iN z?jt`)5wEE+_m^q-4)l|(2a9=<%Rgl+nT?OO(`j07v4;?_2CHv)-dK?Mot}5*zkLUJ zzdb+2WbMOs!Pi@TtishOCHcS>OW?g#9j<*m1};TA16Vabn`+1C-nAsrhH24V{&KM) zvIZ6*y-uipTu&suUXyI{mjZ8?YO1UsH-2mNhoE)xdXAfx$h?a`EVp)1>HrtFdpRv^ z6k6!6v2NxGY^A?am?~R-h+2Qji5kRJ`}8<|(uw!odp|CBQL}-XT7O+p`C-1%%B}*1uFZhJ z{kYm31KM9df@qFpN!y13Ud;I$+f7_ELek5d~{Wz2V zm>q7)$4XMrZ{)@`G|d)+Wjew2yOs?#_|VLo-$^2!txrY~sak-V5k?KN#$8SIPCci`s~mij=fy=0 z<*w_qyxHoCCV!v+cnM($nyct|pLrj9aXPfjbs4dR_i2X@T+g2ssZp)4x9+l4TFMDg$$oYqVE z;};)=hxG7ixZl|>-lv7&+pTYZiM%T9#?0N`m!R`<+Fc$l=HfjbOcPDEsMKvQC-prr z4c>WXPVC%rY0pJOMR{7gwLXVF3_fv8Iz6pix=;M>48fy6kjW)W_g20Qh8zo~@gA4p zJa(9MwN2;tRgih>4J*3va`zq`OKiWLuYO-M-%p7+IhTB8IokR-30Lhvf1Ph<#@&vI z(Z(!bJ@MIbz3pzX>t1HWem*dT83#ZvR7NQ>*=OS3>&eIhaBsx1^rTlhvhSYUk3X?E7v)$=H^zLl zoE`6)M;(fE`4M%jhchd{?sO%O{>vtV=M7lYft&AXGTY3D6|R)pj5oAxSviT6t(D?FS8QJwaA+}e3FZQZ(B z;rexp3XH@guYmiJLtFDj16)C4<@>>b{u}I*`d}(>DOpMvrrg<9473#@OSYcex_G@( zXe$7f;1ayh>~OcL?Nvjo@)5oxhK&Mi>wm>+Q4c{!YvHx7XE+?Q$xj8<?NP$oO)Qkwa#W-ASF#lMDCe*N0R-#zCFz1 z@sN5AeQsoZCv|(z{~CySy8v=BS7H7^;x^VJ@jJzMofY^kwUJH&aq-d`%`0AYUOP9r z{+vkGchv?DJR9cVC7FyXKc@1=rMs&iR&YXic4W^fH62UyT?RjVM-x~Kx5Hj-Z8aSn zP`CUL)P>BH(Vqr@X*56gn6b7oDlP{mrvM$da|L|#7w58RfwM;jrMipJ$?%Y}9Sd@rpm_%I!~|M z_V>D&qlTvi4eJoC$J>DF?+Dw~7rY=*9lWQnB0?Llm6al&(RmrH@Mk_F(QFCg^H?uI zH5c|*xJ)SQ^thWPzi2-M;k#~d=60*JRml}kd9|xUG~+{aJ%3!fqMOktFz)cMuMNHw zc+UzJ-hF3VeO}_gU^zH}6nnVwH{ZZLaDRUt9C+$@DKFhI$%%OVILr7NuQ;C&7_~V@ z9U=T1juLr8IMHb9lYcNR#IGWML@a+l9ocTE&mqW{n0$QTzjXLC6fqDKPZ7ZaK>7!- z5rq&Fg8jRHhzuTlGm{*UOvH-VPQP%t*uFp0XbdgTQvNV5KQC#CvbXkhUgUW>t)1i< zYiGp8oFBI>44T&*TZ|nT(eX+3q2Kean+8nkDx?Kj);c?PlhETE;&Dy zs%5nhZ|gO5SvGb_7Yd!5TE?cFoIklsgNK1ULGD1X!2MLj>Dje8*6hSpiPP%x9>M=; z)v~ibMzbw0!};E7H9cPIM<9vII&4e9CJ z+QSbc&Xkn%$H_r~p=3(~)t4A%D$jywiWCe}EJi(*$p;8^dH$3sC?pLj(ddLXcrIc| zyg^wk&*5KFE_fcQ?yTNNsgg?w(9kJAZ5J&GIpH_d@)@8I284)YPco1XncvkfURlq{ zt@)nG%msQA_ zfZItm5iQ5XLSOeg9ue5d#6L@0rO~-2-Ow-89PLl3U2Rm$jCY0qB-IoFh)eifR>-^& z8)wqqFh2GocGaGpM=5h<4$3*1`6yDxU@;WzG{@j!Fw$r z_lCSrPuD$PBIurcsaavRmg!&!>Eao^*|^$Pc=6b@Al#&}7kCZRdd+|GtNIIBkBy`z zNC@f&UL`7Kx^7QbeT1v(4-WCKdCEZeN5g@v2kdx|^>KVrPFnk{rJgYI&8J7T_I$CM z@7Vj^^&9|+gVP3XP)nGG1QCxKZZopEm208kvIn`|{!qO2BqEZSoH4l?Wx_8_zv_K= z%EDehFb0!mFQy*U&eT;k-y^X0l88;YgFWuNU#572a>SM zWj5ncJRA`$)Rz3TEEQIJJK#JG*tx5M-sX^a;BJ(@L(nXcYjEZ;W1CxT2oDaL3gcnO ztd6kGv#r8&*H>rv?mfwCdc1o{<9f42=U2JsZFg1W$8rQG*r8EHh{cSlQZWuFSK4T0 z$HdyhoJ9AL(6>(`aqq5iy2aKHktt6CE{v{TE^On`jn;lYb;5a<2|hQp9}I6yV(42i zvPTT?>qqCRw7Lh0HZT?S-8&yHk?0R;yp2(*T|Iu)Z5{TiW9+s)G~yVj8<$II+aRYS3HHyDer-$Au=;lEY}D=E5ikI8$Q!~%I1pi zi+gr7TAcaPUGKmUuL39}-gCy#!gFK65;YBcFBRT}FLw_wXm_;+Z#?~^UE=(?k zSf|J13w+z`PGv-gZ0zgZL-Pq{z_&7^%PKj3Ez>dKNZgIX)kDZz{O9R5gNCS$2v_kK z_mTOeH?~qM7H+eHlX?ay0x}0mORzK>xwg~r{fM_|9p083Jrj4#cHJ4L*jLN7XTu>_ z{5RpVd+Ss_zX?1oRSt;5PLWp}{Metj+$ZUW#VHSmdiP{UJRBwNZ=P>C_)HGcLHC># z_7&~)YhxFbN36PoNA+A_E)Ba52k?X6w*tp*+?>wz3C;XtjD)qwM?KgmCn`;j$F{*>3x+-lG8g!)uHI;|IhEr|K6dMYleU{DW=6g$2*>M**P5;a5`qdUU1r*f!p{2`&o*>KdB2j3!FYEL5i$rO_kV*=)lMQZ~Js*MWeu zUZp~6`1+I$!azVr*JXFLZX_hvgXuN;8=7UuyhAscusz+_k)+-jV0X?ULanaDW4-X$ zXoEto7zPyM)F6Zi)^v`1>l{O>4p9i^8J)Gv*$AVWTjQnS1@F#2Y^H;7B0MFI79Ij~ zfJ>U0D|OKEBEe+^-Bo(^W9)#U@Vm^5ubE`-lo1XDTkj*b zy1hI`{tp%P?yZ&gAvefG%UTtC>ZOA`72)C-Qvu+isO@xhXPS{`nYD4o!wj*<2H3co z9HFMyNoT8imXns{XqO`r?U1&kt=7$2UXMh>mjjo&u@uN=c z^7~@;(X~@Dn$3*g2xQ|87X5)A>#ipeq=MlFO@wtlZD!;HCS3LN!^D{u!il)Ki!(@k z5O5s3z{aDltzY%{DSko8!d(iZty{vBJ_f@>2jRC3i?b1q@(SpxCWBU@L}2D14?YHd z*XV8{T7bWBZ`<1AqyzED^86r7pV$>-d@bVpIHKvee3;4zll zh9$5@fkB0~6VOk}(=LM7Op)pSs@^Fp`v&z1d0MiOG$X1yfvLp5g9I(b2G1Z`U~oye zpp5302pREadfX{eD@(rjpE%b)nb`YI>%z&&Y(ZtkH68QU zLO2E^jmzP%DiZ!(HFUIuaIVGfbi>UEv#IwH!D+2_>t;OB7-@xnqX?_W$>Pg4A^~;4 z6C4}5%?fG4=eAkjFEtK_lDxV@gM0AYx9=AX#F6WLC(OnWy0k;Agq8ZFk`efcPZl-5 z%KE=fV>pOIq%(5adtbTl;20!rNUa(LFRC0)mP2dms(bdyZ=V^MEO3QQ?;LT10I-7e zvGF)~H(gRTgL1`!0tX3I_~|a8t7ew`*H1>=krVUmrIs)95Q-5q;?fUz57cJi!a~PyuLW48pz`RI$tJ_| ztGS8jKM6VwliG#KB!x=uQevEA7-3w)u+AcmEhNjK$JtWs3&G;Q@}j_0zooJ8pp&jl z9d6l1d?bpZi(*xpG?0}FR_+n{1R={1!hzrZY@@?cXxuEQN)b~;A-zE~(_gOb5&nSp z6!c)E8rWB``MKG}ho7T;^s@kUJMU8NB`<9lCBQ1OP%dI2{~@2G zD;VL~PnJfwXb1cN1G9mxKtIWy6%PX)xd%) zwRyJ^7)bUU@~vi*-BEu1x^=4GLqkI2{TeAM5p309lh&6uGZ2f!)~x%DJD17UJp{Y^ zX?PXZfw%#aVW@}=b9>v8dJ^YJKaV@hE)f1suet+pFdxX_?gsgz!LM6JBLY|{O>;&W zBImSrhuzJx>E7o^41gLPS79}7joP=MEjG^7)zTaL!VcA3?Fmg4zAfX~%U^k5+6iBY zfc#4CvLj|6pmGEKkrE_87#OO*Y~1UwD>A~10>yYqkxQGAY)8E82=|5F6XS+SGEeY# zqbM5ocAcook7kGK@Mw;ZQ&^(*#hkN_Gn{J0TF*pWFnW!K*tsxs<7ud$45on}a=iH4 z+k)f~tcj|Qj-#B`L2y`FzPTLEj&ImKT`u{)z1WeBF%w+}S>F4#E^`n>VNE+8ZX3Wk zzcl#B-ZdM$bh5+0spw?sL(yA(JM#WG+<#2=3QP=ojsfL@A{vbRinsW+?KQ^MI7!pV zvGW>PAAKz4s8hw=d;Bz4M##S`v=%A(SOu4~p4<(p`J0C?+YYOiJF^Q7{i>L`S#I

4eAUb5&wLqtZuY7re zXKOJ5i1M4Qi!s>5?8n?kl&RNQlR_Tmn2=knH0JFD`Tly1x$N1`Fyst6Wx{?1jU$50 z!dp3RzqdNkaZh|bK!a1?E_j%1H??IJx<=Yf)1kh;lub8!ga04`rW4)(e=R(+t*fzJa;bp@O9ZNz zNkUWHY;5*Z6&<*#`z3j-)(|uvh!N3^;6A@v$}zh9*3C61I#_Pz8P3}hpgoU# z5F$t#5*7Uc`ZCc0*Bvo!r%?-csu1%y6lJv3V`7BJh_)WmVC`}`D3g{kEvPKALY2WSjCvD7KQJUXDTVtn8~z!tePPsi zx>~3_daK#7)MIJ!k_VI$GA)w$*EF;n z4mLdx?c_cBjbot)mGykdd|rmrim4sffm6o;8SY>-*p(%QJkw9p2$){ui_S(8#ooNC zI{mnc`Blqbbab34u}0}P+`*29Py<}pjaF-<-#O)v7H?Pe zhVaNS2819U?s=UuE!7Xd3R?B$DBNG+mDK6?@`FDcFOsAw)Wu;}t_SAH**sA;yWmbw z#&%jVg%Wxfs6_J`(4H;Zk-6YMjgmkD&Rgg%Fk+tj4oGEY&HJauf!9q|Vq6DlJ#`st z=y&4^2ic8VSKUV>aM|Owo=rHvW@xfXRi;Glngz`Kf32N+IFs)m$65PD5teikMx{_W zblBGvl2Zsp8x@m8nro%ZAsd#+VMs*|&2&PugF|HG&;eOuLy?SrVKOWXvzh#!`r{k_ z|DNmf$Nk6syx-S-J=b%;KF{a6@8|y0nf|9U`>WFrB`fr`3eO+M%X*%+ysk9c*Jp-C zb_TUsjq!La@8d^=!S4?EFt-|;$qnj#%&Ht*yXa7;O&-fU^;%70R@o&9r;87to&T0{ zAk9xNVmOkEtGCnp>DA40ZkTbXYo~RkWN}SX6VJ6JUK{nVv8%;c$j&&>uRgd$8@@a+ z&BWBfO(gi4x>w+7jseH4wcDAP=awK6{}{P8%9|G%jDmaS8)rSi)|Sg6UkvtZg^@{Z z8)P9W{|2FK{T*WzLRwuVpAhpm4v=GrVqSS?whk2ZH(U`Q6W)g;)TT}U-VZbgJk>kK z1GrGQ+cGyxFv}sI`drU(>Z50V+_q<{;uWvFfF9MO)6g#tgZ3CN7N7`5-l0qW>h))W zchygO)!(J1tJKTkF{WS-O?=~Bvp1|>>Uhh|FltT(*eclPJX!kQ@OaNd2Hu8 zgXGh*)5TG|dwj#FI`d@9I%;^32|lta8gwg)!#)1dvh1^Snlsn!c6 z6@rF#NgJb#ImN_!7JGyUsU?&kdLP%*fUN|1e^f?pHOYYm(SXAd)3}L@fvStio2rJf z3UB#cRC*EMzhjccJ8}MEdl2?>Th=z>ZbSqULhw~dpzG(-q7b zliU}-bdr{Lc>2WWV&LZ!4S@lQ*N}_8n4@D-xI3c4Shz=(XdLZ6LZ|z)nSx^4S+0Y{ zI8_HU3~g|Tf8g{`fK2D^y=8uXP&4EsU$>!SP=VK~_~FU^wOdd}$``}Zu62&=SzCQo zFx!oQDdKCAv@Nqf1~2uQ5$zu@@mgOs;b)pH+*=n%ZLRaS%DdBy>jf=#wQuIxv*xrW zEL_sVZrEERMBGki{IM((Nf$|1`rNI7<8Ba8a*a51z%+=yurTD~Om}wA}{k0s<00t<>?3iCbO!S1WGeDWRkN_{P0@phoDV z%epdVM5jVykvBHUGI^(KhN@4Zi}ym$F~Pfg&QqS-=GH2UW@KZzeV%UIT1SkioWD@f zBS$y%-U@mfb+ru5t;SaM>>}f~5yQ>D9c;`x1tD0e_Dr{(eZ-1YI`WEry4W1DTb>ZQ zptYH=y~D`{oOJXXQ1x>==DDd~SW?`?ymvaVmEl=>KoK^W^775w3mb*yXszph%#*PJ zeluqjK^kxk(i&`ofs5RAVL=LJN~#{Kvn5;**H}g62fYT@Nl$6t+B-iLO{;_7^F3{y zzZo3*1^ZG3L81vr_#*VdOo|jC>c>WgBWBo|(U-71W_sRTt6vuziT))&PC(>|*+vy7 z52^2a{gjb@Oi~Se`2ka3$aN=scVHxtOQH#2Uws5Tb*hCCR~^rX058zwZRLLDLW2Qm zs%sZB5S;pxbM{M6hYa3pu6E{)P(XsA!fJkUbcYuDCtG`shTa%Vv4$R^D#o36wKsGR zCnDPM>!%O;%@<9^$**z)OxF9HG+`yTGhAqup^_cVTH9^)bzB`lhL~{ObIz&6Q>VHs z&VUG%T$vi*z*=NpSCEMDwQq>w|Bda_W${9impO@>)i5Ahj(whuJmCsCR=LL)bUk<6c_PgJp2|C4FXU{JrK7g%X=7oU zc}WL;?m>N3r$m`0k)|G|l$*_$%n62nzzk!;h}?Yzp{<= zLs}DeG3m{s9zrjcBELh`=To;r8-EZ}75bQL7=rQ$h*g6PEO9ecyzQOv?QYIU+h=~b z-ZL_wWK8x1!jOT&llbP+MLF{swAZCv&v(InU1JJY)lO@JTELeAb^)_zM8Mn6m(OWe zQix);b*iX^8AkhF@x&4zHih@bXj$?kLMXldx_+gx_4%rQr zHEFVO%snCs{fBcy^gNhIEZ3nX$!|!OQBrX1%$?Y3!YfaIerIAx`D3A?<{{Qp;co^Q z0ab7-3`F3{eCKeHK-a&EA5q+Psr7iYlxeL`CRoB{w+KgZFOVw~^fLj(CgBfM8!yAL zEW~~n^ny7lZ7ykrLgQ~rm!{dmbeG{63K|9c3WXf+6$(Wj0HqD)I6sjE*>JbMfVwmKMD_-B@(@GMvjyd*+t{lk zMmr#;yYC-2t~`L8t8C7SPE#M4@2j3f5)B{0Ro8$8iIHd8Y+Jm=c@;zLrY+{4JN4nh-6y}_ zxV`sO;jZIBUb;J#Q756tl zTWWW5@q@U|zO<=-%*aVsaL*2yYnZ$Y0j>eJ8`Uz743o*EkFV6K=Ih*sa?}wbs69L) zud@Sa=SZJEeR5qTm73DWHKO=m)jk{K|rF-Ei8=v{OZgr zY@eDpcslr+Nh`*m`lIShn=2ocw{GK{VL{Obdxc)2@O5UW%H`|3r@OmZ7p+??GlYk> zYGLF)UOidvCRI@3VWwipW^=Gq=js5Z#?=(P zVgT6+M=61C!QYMLSlfj?f}=qol1bT~U!6Ulf^Zn4)i zH#gTRDADeE?zA6P%Uh_|&;AxERXN{!Vzo|6pddj)Pd!;fhn6aBR|%{9V6VC7xt9$6%bUN3nB!gv`<{Uz9^@1k+2R`()eqvz zWE4HV-Mk)602EYZ41>xgW8og;=R_wj_&33HZFMafwQT43bCdF1IBu?slIsTfi*2Ha zsRP9+9?~deDQB-INy-KeXZTq9x$4u-uc8QDX<$;L58d0bHGd=?)gl{3C^jtYix<;2 zg27-9SkB-b=(-f4|*2^!)b$d;8eLCdZx_;Y2*g5RmmhwY(rAqPWf}q0u zV1=6XSlGC*YH)MU<2)Gr411iJ^9NBJ_V8W*^Wl)pmMc`M+{0w`k!(f+4f{oLL8UF= zt%wa5XW7UD*i;c|)rA1S#9ZDz*{4`~Y6P`0(*Cr@b%1s;s=9TkB`6RL>v(#vpCJ-+ zmT%z;M3;;%EhS>xdQJ9RYGRPME&~yXi{lf(3wQ$X0(1(z0JGuxaSl!_7?TVbk#l$$cEVBk>u94!iOQtAMC?*=;hEg#YS%AFt!tyHr^gS|Vplec-N5x$^Im$0I?T(%TfRCJd^;)f zv=6J3+Py;Go3VzdnpWNyEVVgrJkgqEy6OC#z1Kpr9(zUjx{Ca`7S(TV3!)Q%nI~k~y$_sxO)qQn(*hJI2K7A)~UU>%oToL{5_8R4jgW)=P zp9t{_WYeFdeB(bthh-TBCl!c+u zw-T7ry6&4dovYf1>uafxx{s&L#d=MDwF~5)k8iK&zC0G6bs2`Qxma6pW~Tm0+7Szw zhx!}&3xA}%tr&3^>>Mg{4UDpkZo|uEp!yq#!+E2uo`cLz_k+E}o(xlIh&}wp09jPt z>q}FvZo17gc0yD@_RYRqGVNZ{V=?{t9`=1!RnzZy^WS@~%$sj~A+SCOfQS&CJ!fdj z=WzbxRyv`I&9wdG`S%kyj`%sv4NP1Tr9{B$>_Re52#E@Iu|A}Q)n7!OJM;dF=hr)Q z&9@$uh|-Ejq$4Tk_E`*g!{7J&eEMpW!FWC#5zT04ws%_xeJTpBc~nhOoOd%X2;N#e zzhPd$BuKqw#WFOLogX!*_0(ijCSsI&%Fi@_{ZvFGXV3IPB+G{Ok>|S$<|zG-hN_R8 zy;<8W$9q5HRYmuUF;72X-x;gyx_H^0DUSW(j~O-Hf2MZCb{givun5ckn(h}L(*77Y zXam94**d^Q9A#)BoBl7>W@f`o=rb7#IwM%(E9*|9QFb%|vutAA6c95i0=SC<+^ve= z#xJy%?{3EOZuzu7h!D%{$Rk~4gHR(_rPkZ1KV8YG^%AebofktQ8tlJBv7;XdB`nQh z`;Z}a9k>rxmO2!T&m$q3jD6XrwK%*ymDg9nJ&;kk(eqyBpa)682BzgE)eF2^Vi8xo zVtvV}<$H-XE=_i&qBAQtuF#yyq%uN7r5pq_11$`0@~H6X$5~8#uM?*i8~OV$lF?TE zB~xQ^GY+3$ zr(v$B<#F+JM&p)cAZje0QGuBv&xz_p%Me549e96|ix4QqDL(a}fuKNAHCU`{`~vB0ihhQd zv%>x1J7sCnmK~WmVOSaLmX1}NS2ybn227BnCa6!KkAfjj6(jb`1q4BYK~FD+0D$f$ z)Mz^JB25L;*peO2q&2Sh@9=ROznIiOdCD+bcF8CSsItN)PsgXrQx_Ii^OGNLtPSP| zx{TrI#kGtIIUyU2yACersEk-amvKmE;?Wx#_vFQ&A0{?BGtH%~?0Z{sb=f`2bEy-e zRJ#t5ra94QibgSDh>{bUm2ewalGesn#}fN&^=e3!l>`a1X)0? zoMvuI<&~SzEYqvGWWtWy{9`u#^O+A0)q-WV!RuNiGGGE;3T?q6Rsxr39S%@rh`2psmioBwFi zW-Tm|4cVW@*az81%1?hM|2|fh%G>YSI`P{vlDv3F>M9~>cK6vC{|nAB)zGOZJxG=G zc&5$XPL`2v!4m*13#SB-;q(!gbwq|S(?U2-y%1sc{f;c9F9XE%KUFyT^mInYp>s}X zBljI0<4F=Fmd?{mJy;9RmZt~48#|IWevmB^io^%G7tG$el%l?>ORiGssSU{WKF;<# z>X)e-KwJ|(#;!m5?QzPxt+R%UN2!LPFnIq!YaL@~>#J4P^i_(t>Lk=EKasJo$xu#3 z@ARw^57~t$V-hvEboxc0<5O1>0$Uf+P4ov=&6rFagnS|aR-w&1wt5z|?5VRy(@S0{T z(W?QKCA0pqWA*ohkjfzhNs_UL!yW3Xa_bt*Ob8TO^zsdBi8#r6vSGtbzC1=7q%MDr zXLvL`7_VB1rdxU4BlBC>9Vt|jq$Vt}%V#&7Tz6S!ms_+&>9qtDCb{D9kMG3WT;mo1 zMd^w0BlFUpG||scC&s{3YJ9sUyS-Wy*+FZxOwYYGnQo?So_xbm$Fe2Evz_(( zF3xROS;%S|YtUXg-X8AeGZ^k%O)XcVp7HB0(<`V63cqb2tZ#NOB}%Us>gzF?bI!}S zD+)0i#;lt^MAaKe&Y0cdtCuUPqsD=U%L^FVpMdq25XJ?lQmAwbrJa^&!UuyWMv=?CoT*p zzT7E1b&{GOBqNK<#&d^gk_TYd0G>oOZ1JYd z8u@HN^mt-lFxpR1?E;;$>CZX})KMM+pn*~6R+q>l%j+4oj_`O$M!1r>H?V>|Tq}X? zr`Xrx|Nb&W6AUSE(`gB$NE)Ik1KD-`_g&X<5l|}!cewJ3kUaC*nzFaK z^?Wtl&Mud{%$s=N2W$QKS4@$oaXiC+rPawNL!@{#ep*S@`~~9_RSmfR_oNhtf8fS% zKEZlEwXRLFZkzX46q)q-9vmx=gp-gha#iP6{P@>l?~k~`FEc^5VfqK0inX!Xdt#8;z0~4M1=+-dc};&4@erkxt1j zJM=p)B8rEzf1^akD^orJ5Jyy;JJxgoeRJp=AeJ>_+$0OD@@hHA>w%u%GP-a<1o+oB z7Y9!D0t`W46fA723OtnQu=c4l!>C&|x)s3wQ^38?I%FXFBmKEcHt_nzpF4?VoU<4u zS7-%UR+*_xHI`C3m#C(7B~_|Hhkq^EL1DFp?H40bPnm~)=TiIEDJ(gXK6lWdBN0fD zb<#}Mj&kl%;`zC80i+C-He{}%{1i*;+(}gf{0}%n(OIgfn$h}dm$VGPo;cFr9J6RW z3c(L5X>8jTA-0WWxsDX3y~hX{O$3|DP^%U(cWTya&`We(W*zAZQ`hC5PlDn`p`;At zv;R*ywXqT{2Yf^Ld?)20ww{WOGG<|qUNu-`teW~CvdP%WrDFl&wJ z=R_=x{HgS0M%>&C4>;{L^34jzP zZ-&zRiap%ke8VI{a0S_2&5+*h)BV%7hgnB63wUUK85AVhP@B~M8nD!&vbo01ubONi z*Y>6KjSPu(it2eL&&@2+P6KASR|b-gLm!Xn57&T#Hv8>Wd`-#Yd%UQQ0X`m+Kg|O; zS`4cR1!A#}&_+!!t@$KEw#5e6FFguj9!+G2!!kx*BEBPmlniT=KV5KN+Z$p)nJHP@L*m{h$rILbBouZR<`g5|4{_? zGGyWVyF>1NMP=MP+upuQ09pA#^F#$@|EVfF0~CyAQo~$^(U+h1V~Qw>&h&9(Od9|)PPlxx?oC(6H$iAMw(6!$Rsfr zo#CEyB(XjCz;Hyw(o9L7KDnLsx5w8D9dW*Sf_eWMPkYO4W5e=}b~5E>n$dG6=?TMs z;=5@hpD7z7vM5T#Z?X%_nK;xtZ*5mXk$@`fQ<_8?6vI5(|Kah^5a|tuh}hnXL1pHT zZVd^R)++bD?@-lh>~hEYnwf^kDfAV76M$MMN_*Gum!OJ1YZurA={72{pB>V5VOCh| z_Ok7S!VHbwYv}_rRt7{zm}A?oy?(SoGWPx9Gm<#>6)h=(=H3D zL8^bng(_;*jRvT@lS}i%$*1zyc4}2@MC(XkAB&O)(*f1q-8RXXB_J6Y`IsxCW<&mn zX*B7YnIqKZ9y(7t1Jg zhm0U->+B3vEhZ;FcJH@wI0g5CH;IxS;N2y{45>HXJ{L&_Sv!9JBK|!Z9fUkh;^qOf)QtC$}o$ z2mAp(xVw!tCacKqSy3b{3&=}=Zb83a;AN#?y}P7jzqu*deJpH)yLbB4qJ*@<~GCiuE<-(vxgj-Y(V z{~c_ocmIHmee{VK#_BNZ@tlGi?FiF9KubY)P%lCtK0B)l{UUHNw`d>T= z=jM<9$q+hO{}Vy>a-jPcE}ZXRjeRaX@GEv)1~B~JWa4_yoFxiB9bZzmu@zV!;Eq2Z z#j}l0MaooB@j`&C6|=KuRjA%UDkv)zjjYXf@c)Gw4Jr8Dt~E`;hm|!o#)1DuijIWw z&b$>PFd3>u4j=kSdH`EET{aC8ete2O=E&nOwa6*Ta9|}TCyECHsP$*8`1r|P2Ix)v z3_NHVNN=PoMImB2s%Mia>++|;e?p)`cWES^F0TS5NTvk?K*206#NCIxai4S8w*Cu2 z=I3w{E=QNo2L1Q^7C*chAXBPMK)@p4(GSJ`CO&`w)+ek%13WTEbr&p=!}BUyLjNX( z1>3Zb6UL_$fLs9(+%KuG-NI>v7sT%AvAGZbNs(?j!DUvO`XT}qruOvwM}*Z324Xno z7nwcg4hPcu6aNV8ub2)n(quTACTbyV0eSx`FoZE) zMQ@XI^ZW42|K^^*6!ZUQclzxDNCRneF(-KXFxUmBXVLc(Bs)D#vfweW%Kk?mKs3JN znh-!$0{2N_rElv2*s5EtjFSj;K-U(RD3F=1X_Cdx+CKi<8wY=?r8y4UCq!m=g4n zn4sUO)U-d+?gTT!v$c+yq<8*|sex4LB|nx$0@J9ZJpQ88zsqI|X^bPeyiYi!WUYy@ zFMd7>k(zCKJlYVbQ9fhrbFHds=|M#!x)((Fq@X%UJ9h5LYw5QAb6XKps*0Ip*19dm zLI^}zu>Bv^DOV{}nvy&1{Dri&KxcoFd2J`((zU}yQIn_scK5=Reoif^x+Qxp(C4Yc z^PJ=rslZj#PTF9H*?BMQ0qXnbk-xNlAAZH-Crz3H`XCaLzg&RR%K6m}{n;5IQXaUt z7;w6ZVS5hs5=%`38^8JZ`IQ3QveF}>>P0}K3D@7L4wG;RE~Z_1{QYJ6Wt&hz<;Uge z^00JHXU&ZEeX!Zz3+)hzQtSBU)K{eM;16w}oh2=H7;6+~n`$6LjCm#Q^es9BjJ3s8 zDx)oZ5^4V9CAou>$8d$n3b!E{V`I|dcd;}{v);w;`9MdhgwrRzU|Kd-omI4&U!vCE zV-Hg80@U@Dv+0M*S>lw-4F51(lUXiDP$q z{(NYsJ|1W(_I+!OWUqfZY7v14x?X)i_t;BOWIXTh<+4-)okdC-DeckvgQ7ioVu$X22qf>=c z{W}~Pq19OlGa&!@nP&w1xWuw14lw;9Aq$ z3jzwUMVj&>g9l4pC%V!UpPn^DlbU67m=m2NPq4}6zBv;qB{4-x2v6b>riOu= zaHysI`vCt$;=eVZNkZGBnxQu%ctDc>SildVa9V%=pIU;PF9JPV;PszQ;XjxEAD?pf z5fyxYMD7rGsvXd6yn7s1sI++Z(Cfy6xPjK@$LIiRvY>g8l_*-8)18bhVH9srF*CR7 z=ZIxF#~#s($&Ck_DD!=7bQrcNn&Y5efrF~yt8Qp+?|olU|96pA@za1|_;=%gYA9(U+yrdBK^$zw)~ zCefukLCdcP`m*(je$`W}+AhNZ!cBSjgTO&fL=zI&Eq#1r4T0vQ&<$(H$&lV*R4Mkj z2+JTu9PI$L?6;jMa8#MM?cBMsX`o#vf6QZdVq{B*#e(_7c)XZ&lEVnYcVlRiXWp?2 zdI7O)gogPSs$oND<09ZQuUDBUDE;}7KgO8D!xoqhzol;~7%6|1wWsto@R0i>6+P`< zJI>@u3{Io#sr}h(NMAqxL0ju+(=}DU*CQp3>XJ*N3r=N&rdg|Xa!l;Qse$;7wpLbZ zhs=bAilgY_cPT-C6;C^aroakP1*w@VUa#wp0rFtEU@zW_$gPcXS^KHER?|Sg}84pS`j=t?J_-^^uA6GZR4aB7Gd9BTuS-1uFQ(_Haw-B(Zx| z61Pn*^xS`%CPa9d7KNCu`YE?4f;Q;$v=<{KymfTafH_{H6eq7LdB{6I-~qcyH!I-W^r?Bj+>q%vM5(awr zD)g}0*7(u`E7f})TRZO$F4N*?h&?L-0Y2@`mL-ik4#fdgg{YpjMqgNP*=WGaWbCpr zy7IOVvWLyXDWwegDvt(i)++YSN%XL8`{dMR54d)-FHTEydgt6!QJ9(I+`5IyEoi5* zzlzgJ_v@jne$1BOODc$*lIP8lEQa3Pv`_2ZeU;q_$RT6$Z6Ut)%lm3!FLXX6-O`D& zcg=gDU6SqsX-wGAk&!~UVP2^w*qt7dMw^3n_3113qBbnO($;^({)j;i#jsk^{524`wVpID$vB<~Xp zzhqHc1Yi!`0ho|E-h@p~J}&1qp@V_@`1qhb@v~CT|B_B>O_T#C-AX270_%X0m-_zT zrKwLdPH8&oC@97}MBaPqZcPw-dB(}p4P$o+Zv)_Afd8EU4kG70fPeVhH!whtd~I2d zeEjYCjMD{^*LRhCXFj6GPh&jCJ7%0pO;Ek_4j7b0T}i%n%CfYe9%MDo<>5nHR1oy0oMuAW7+Ah-MU`_+kN5$02Tfpv5HFODO@4VB=JF}$)*6$FP%Q(H>B?6?Q`Cf7MllLy49xuW^F1(o$eH9JoVjqy z@r^2T-n!)3E;BqfyZu4x@}$DPx3CX@tQml;IZM194-?GkkT5$beOD$w{Qwu&Nj;oK zC7;Bd%r-}<&`!wmpk3n&$DYiqLH%H>%-m(?VPtiXv#V=l7i-LR&pRF6OkYEsM9D>Q zpoPZT%RzfmkdYU#rLlI_?c`UlUR}3t3!8O$0>C%z;D-X;jDT+ID#z%D^wVZ1i+cl> zhn#>0sB=hV^kjCRWOcOO=gn`ixc8GiSzf(!Y0}Pvu+kBG_=js+Nv~3s%>3SmAHpOj z$s1QT+vi*v0#;eL4_^jmw#fa@_Qk5#ymcUCtCUXe)_p(HXtMBPO>)S1<(7d+;!)KC!X>~fOF+9MqJsKc!4;#+aWSL;>Y8h_RUhTNM2z;etj&X zFR{lKnoqVbwT10KLovhdE%%DQ#qSyWA(torFT*w4Udega`V0`>9&{8!w$xL#h2 z(wn$mH@wKY7Tqk_zq^ZU5`sBf@QRJY7aGs5h`i^F=mCwxuu>=2D!v?`=30L?xngdZ z^d+mtMds8B|B+)M3^pQ;INp2YVh`R9ei>-plvVHlqM(1*fmC+7v{6 z5wMZw9I#4xX!*HR=k3BOC8Jir$1)TzQy)a83Un=}_JflQA1GJr0*&SP{*VXlyO`7N z%S%@Cu46SRVoMzxJ&0r)^YAA1tPeoW*Q%6Ix-?GVdT~6EGlh8VQxt)0tVfIN1uDv= zKn?w*5_W?o(YQwcz{5u)jMa2Sg3rlq$(j(Fe8zKvL+Bl1K{mSYS!WvV2hcM&roCu3 z;<=DgxtH}=Xb|` z-z4-enG-@Rdp58&n2xPUKLjnPoo;>U1WGkwLtR)82)m(zpdkDf(`v04q>8d;t0o-C zrd%21KhiMh-&wc*Ri}h>vVk8t*OZ0nT1Z{E{Wl0K{5 z7=WxSTHnP)8hNQ9RJ%dP&mPZ-!ZlD!y|Qwq&N$qM@mL1nZFkdf8iI3L zVTSV<{u61Fggu;6Potm?R6A_wis31GT~P2>^sJVoW7VP7GTQ#0tqnry(mG|!79-D< zrGPG>z@Vdrx-a%~q#*c(3hhfG5uiZaf zFVL~l0Jgo^`L#g>xWFm20Dk~*=0P_;vjgJXzw^#*p1n<|&j})dMEQ!oH1|uLYNK9h zfIgqyN9ARfzd6Yd>e$D*kpXk?s#=Ei52dLnFex{?V&!1=z4Gsc)&y z1DT$YL5dOpFF6?-8)KG_d`50zoHGDqcA@nZyL^0k-DRD{Z~>xgN8;DYtM`8b@*xn! z@{q8i5scEJ!cLnB?0;<*5K`qtm@__}u*V-0xbhIAd({!hU%MMJf%DwGMqM0B&~pJ3 zhlYwLda_C<(dIQ;D9Z+ejEJ9@>^R`RHeFcrHEuu>hp+CA6V)gIQeS)B_9_A-!%N&@ zq|wJ^bA7d>rp6e^v7I_EUbLuvowCQ@-4)&s;IrHbMep0X>9Wr`^1F6b0GXVe3|Z8O z7R(1`kE_xWY~F`Es3y1p0ImazMmT5K;bTZc1(AGGG(vCD4MDJDRHNs()|6AFJ9bZq z*|#V?4*p{7qkah6!5*G7!6Tx45B%uMwP=*|<)Gs=-2llcx)pi6*n^652A?8nHVEz~ zF1#hI?-4HsT2jC!(gZ>6JvSYo>qCDy*HJ0ql&;&3XHtO)tpX&FHLrBM8B?N)3a+!k z&Oe5%jb1X$IQW<#3bV^Q-$j|g_2DMtG(_|P<~97~wgvx|Qvpm8+I-Kd(OVN+hC-1J zMT9xTpO<$RAj?B~g8>$xJMRVHqzMeJBM6Mdxn2d#=Vcpia&DvvctA~JHFl`U&m9dE z+nsv)NNOY!2~&Ftlc^32UQ2BnSd9aYr7JCHd&NGZmr5UNPYCL$w!d^ zIbZ&_N&gRgybSs83_9@r|23PmlW=*%)v@E4-yJ?W{F`#jCZtzUzI8o#$7RT0AfIzv zClM#E=7{TMzvhgd18k&#MyDHYZeFr^`a5NN(JgMC(=k3#>V-;99=Samf*9c zPX_&~T7xXzlymJbV$uhMQuIod^~awnhFBu>G9}5gskNYd8z2O@OnrP6V15`obk%EO z>VWsFO{aw*xK=x>ZS{h}Zos?c6ckF2T`)=f5Tc0V?igjYIk^s$b&ra%voN2Xb+QzV zvy}a1l&t40$9aN2*L%1yzO@FlSoV$g+^jw2Cti3bt7BT@w5s>&Th)a&(IJ={rNzqn zcc8#f225oq(0Jrs*iM9OV_>-3fHT+6cWrA;Gh}^%TmI3PH#57jxw&fCWasapAV+u} z+%uEI=Ai;lF6Y!rQHuR4{>YUEim{n7)=o}^g3`86`W?+7n*Q&W1^iBgP9o?G>FNC! z^!j{t$Oy&c#Yuvxp6W(5@N)zx+96?mE)+{II-eRIz6TSPnex_4c_!rKMY3J*SWsc5 za9V)4AHX~tKg&?$6MpxBzYE3!A3HaL$L~ER`)y!fm_y3e8w8(lYC=G~qERI$y)ALz zlFZ63pz^fj5lj6lyp`v0kSO3A{9B@%aY}PCsC>QL1Z7TgvRXAf0d~~!HEY=0R-K=J zYb(}%b7`9BBmSP9kN`3*o9+V&TeP6SMS#v`j0AtqYIZd??OVpjYPhbqtU@)VxS7z(avQ3=1*9UM zX*@FB3r~b2$`|oW)`#iJkr-#T4ZgCe$zrSEW{S>uP%kBG+E4g%U21v?G@!lc)0XP* z$+OZ~pd9O9)rB?S7aL}F7x*4XUqr$eG|>kFj}}mQm)S`hk4b8k)@Zl~{!%@qXq?)C zq;Ea7mohE0EOsPhqjakOcGY^C`@3X&Ml6;*d=DN=be>i}DV`pH1U-?<;P zr**GT7_1aMt$R3$Xw{=YGBJZe9jUl3dBT(1)XEB$hs7g~HTSO8o)~ z=^ODg@*lHoc=Ywg%z|czo!a-n{hh4(6UZx8s~xPq{{H3jg?>l07n;0QySWk8pSP~v ztHoCnzAR%_a{PdwYpk)4^M2ieJvrhBcMHLm;V-$Y)-M#AiFuEA91Ajx&C9H%_(=jg zS>JW`0##J?;QT|gVXa+yGhdA6WMe?8*QHiZr)AraTmXR?|O6M$ZO+ini{d0ucwQ6wik__D7EE`fI>Kvmi&Tn_|_s!Ve5m z(SEq%aEtxonC%7p^#Z6b0h#tYs>yF&KG@m=k6&UZ$xA}T-+6x?6Th{zHhJq3$TT?R zwKKo1Ni*f?)ORGpHV?Bfm}t0(&C9G54l(vj-$L?(zFdVYUFLdrQ}WJ@tH~P-JZL^) z_M8upIeYea=P@>pmkKFjufAa!4eov-CGxWJpiQi09p4Owgxo~45WD^ZTSPQHHh=Q& zAa9WKhT3Jl-0uxh&JN%LgWj?!p#IPn2_2}& z*Riw9XGYA*9M|jPw^)UMbTHL9IGm@-;ohC<;m6~++uC<7da7h^&TES_*99r-iCcL zD?>UdzWn~%PzZsdsg0%&8t4ZQThwYtpxf)ZWf$`LWY<(mQmbWIzc|`Rs>|xxbAq03afW> zo4R&uH>2~7pGl)a?atqd7EZ8tJ2_-}S#EaC@=OLzHUl_!fWvt;+cq!d_1r6yMApsd ziX>nE(8;jrCQo6Z3wJ^+EG*F6r^Hu-!zd~IN#%22%Iu2T{i%mKe|LoUDsJje7SiG5 zc?HTiLUGe$3tZco(IGV?PUiOh?V3nQqb2_Jb3vj6s-jxi5?{SPrue9wZxns-nhje^ z>HRmE+C)wC=b;xvk)_bv|E#TM*^t|lkhkU)^dV$$rN)D*9t7=Jk>q(KD@-^$_6nX| z%p-`bjtS{oomQkUSF#B|B`&L;QM}%et;ZFshLdPdm6In+Ev1^gl}^gR#drt{oz=ui zbSu%$zFX=*zP7K+qvhTOdcsjC*-l~;7$uw#Ao1F)>H?Kl~y5e}+SqG>{_N5QtV67diQeUq^fHbL1G++LXUPZ_I$G4I^ jPOwCFY_}galDByB9r1r?D=W5f=%wbZJ2%U2+zWuqT5xa(PvGF-aZnJzoy|VZ zZSWtwyOz8Z{NfNx5cmPrMM2*k4(=%~>>oT_W;P)x@ZDBN&qGgDMaaV0kdW z3QDlL`#5=+d9ymXQ~#%t|Faz#D|ZVwTNe*oXD13+yJqIjo*tr9RIrZz`_F&I>0t}~ zpProD|Le8D3$nwWuye9;u>W`4psEP$u8_2|ql=rByE|xKj927e$^Xl}|LNyH|Q*ZZKSAFbep~LO~Ib??!<@ z1^(6q2cL%whrn(}b|N55BQqhxZc`vUMNuFxC;9)Rf<%BBR)`jEI=}N;jaiReJejEN zbg`iO?r5HP$1LSs?7i%B;|ANA($9upj;D$Sg5eQ~`5g4P6}6>s6ePAnAy6e%*@JQ6 z-wxkDe(#OKMaCdLzx`GH_jH7vB%{Pc1z{b5cpt9$a>*XCz7#D2Cy4_gMuQEPfL6Bb z4T_Q9QKNJ?I+5@jhb%rvJNlRSN<-Iwww4?1`91b0LlBW)F`%I}^`qK+EAQuSemhje z?YEsEKb|dE?{l_`A1gNoS#5GOG<~x_S)iCi7jSp^Im3gIa)>hjkEGjaVX1j{Cj5*{ zyvQkv@lvLeUJ33QnDyT^J=?mDCp^^~~Q&VnFS15_=KczBsKGqetgHf8o?@ zc6GekW#}=4!vLFfL|RG?=xF*{yI-1&$Io}i-|mhb=9EN{;2Ff=4?;J&Q)EzT8k29F z3K*WicfS3Cmq^RK18E5SaEhSuaeKbnB(FU&hNVuS0zskH=n}%#v|!%xcxxX`#Be}M zNfDrdQ;8EzH~DIJ8Xs|km2{a$dCp%m)7`}OIXpI2h!7ZnCyONx?3xGngP4N*G4m13 z2sGp+Zu8^bslcv3KtUV$Ty|>1!03`JSa6wNxcU+F_s8GI=V#{!Gnk_1 zlep?4waRI1yF{LE&Dt;K+U`$BIDt|A1)a+hfiim)Ww}efzdny;Jw{73f~hILB+}^=zuAf?PD~npyY5|;$dr) zXD+7eQ?fb2o%Gi{+H+mf8uU!O`1w!)Z1nQ5n{qTq_-Z49T<+N~WEKAR#Jp%{k9WUK znk^NvLc*}XGAhV15X(}#OiCj2+RZXB92ds0p^5EU?+$OiZuebmJLEOD?A+`kFmy=B z<7uZwK&7Dw_|U1MnT$r|GOH=i>%1nyV7^de)-`ZHnj_>%Eb80P-ZrbHTr18iF6gTa z!BGb5UaMaeNgD$vtes{Ocw^b8Sc@G}h;u!|W3Nf8oc<3~BzUpGyp^tK8OZ`5%J%?^ ztu*i|@!t=bC@iPT&K)nc{qg6SC_A5#;D4UpNjXW4bIhc*_y4fFqPz63TwL(EBNf@;eO0H#K}?{-ih|Lb zbbsITukA0^?H&rYr*J7) zJ*J6FT_}^iMNnW4`4(^owv5+KiY^(ylTq87qv_}Zn}fh#pTA62OGi9=;xHAa^Z^9| zc0I&&P|pDY5&43&HL)|~D!ovYZZdS@X1j8pRDPWC|=*nri z^~3rJHN(V|*Vuz}`L;6-bamMOkPT<@n&N#+70*AI;sXHmKTkkA7FEn+M38a8y=?Rs zqR|p)s5TE2?=q@rtZ^obI2F=s4x^*z?r3ek~M<$XuXMkZHBQ2=-zdSjYM8Xm+O$h!b{%-{Ou!^AF z1M2+WnA2%{O0FpI?=hwF2bEw*tEB+m5jLoQuYdwx0ZfI`3;2Mb4^1SxzS6KU(mp%Bc@=S%G~vDa+E;D|9Mi3sg<^)Td2P4`}2&^PdUd#BWX z*b0cSQ)|0!`rk@UL3m8?#uUWNYsdB;ev@ zJc=OfNDM}y$&(m}BYjAi^-su){|AYnTYzYZGfHK!=hxuOyuKNH`R`a2=<}KDQD{pB{CfuKn8GEf-x$eo`0Xz6TXXxBGaC5dt?lCE;E_PEa z3cU|F8{@|myIyB)S||iZE`6@|W~F|8b_C2q{ytp&fMz+yiJT6C`CdC5c>f}zVX@vi z&!ro6+40M2Rok)e@Aj)q-1gJ@aUuJ=qc8GHz(Kkj<(?4GBo8>peguf6lszGGrouMM zQ+5bx)y>|>@mdqyjHHf~tA5=s7fZNt$f=Sma#LOuM@5|F{7q1?i5f0)!gYW&;@1u6 zbd+--{Vy_^w`q0zW%qK^veU@Lc%~AXd2&lLn2+cY?deDP^AA2o|GR)Vv>h)Hwf` zH`ytkDZ4ZSa4^?>T#)BBtdA$A<%idg0G<_a%nDu1YPXOPz5D|_+^w*>3=XsIKLg}H z>a0d2Lyyd}cRf5Z8J;pDT(9E0Sh7J`&sU9{AqSBvrnyP6LFM zIg_A(udXS}z_anc_^yjAouChiR-l->k(EVPFPxSRCOAt{7i-ze^=pn!%=)E?j(qq?~cEL6(y;K7r!S95XX+c`kdMPGVwd&Wn(Ye7!T+3{5eYw z_UWWFrm=JTg^RIcn zXGSeJLPVhV-k-q9**kn}N!PsU^<(w^>!1D?GLPW2kb^f3MUq;NCE^x%9Uhi1NEp-L z{DZ8lEY~-BKgYG7=#nZl<X6`e8F{h7gfy?^apzcSOrdi zWio`X9761xses-vqQWr6RY=IU?7)5Am1^h|MGn_c&s*_(p7X|OVvU|ZeJn4-JMNotF`W4xx>3Sghzp~C8Q6b*_sAiB zKA;I#ie95{TauEl(LH?*&zC9@CUUjneu);uzhH_;>}x(Ee!sZ4}n5sLV;?+M-6X|mZqMTUTpuSj39Z-KZh8ONq#Kj*m_X9 zVkP%URa!k}2*q@acmD84hj2$59!^{Wi)VP?UewW34 z)*OGpIZ|0S%ryQ2iA>XAydP&U3n|kQOQ8UN?tFB;R#Rhm+Fmqd1GBtgX*Piz%~TwqRwR&y18KzjQ-(%kj!0cqxMd|jhj!!J(f-)x(0_iy|b ztwZc68*;L&7##!_FG=WaF%_J%RVmLz6fj<+nWAB-~|;JFZlhGN{|G&Y>G`M^svd0 zOyph^_}2|T#0av#H$7ZmY1e)+A$A82SZHppADiHKE>iMk0#_^v)Gu2UDg3E`RpmaC zsAU#Wb$$||PfcvXhSF8`TI+i^u52oDePOeqklesYdRMv|y(K$H4_#na3hG{lJ~htU z?ixmFu}BNX+(7|Mu1E&Ys}eU{%#*|lr>lq z6RBZ8d{+g0LhG4&?LXSV9K>!61d2odyz`%Qpxo+B*@7^F!O4|uXj?Zxo;hZt*Z)`ps|>=^u8y=Q5SCKHR1GC zhYZ7JVx<@VH6>)l{UOLX<>FVRTaOk}v$Xz+_^iCi^VWGJ#haI#@!Y3PbzWbD(6E$(KbnvmxLBpd5gpgsH3C8bGR5G~UQ3t;= z_WX|a&ScKidNjRjLm7l0O!nnyDiAjq=i$WX#>gIG))Vg7vq|2jacO{#T$?)e2)j99cLd?68Nw5dAz=Ze|&BVOJxjrb%OVu++ zEf$b2!1;6;Am2&!k&iLj8y_^2AI)9eS~!k$A4e+Ox=brr5C*;d!IIS(!2UxVi##P5o2mcDCStb_aS|!b$pdm+dsZ2f3JX{mE&eYtx zNDB4U)I!6H^ThyO=+x ze!ttlbRoT{Nj2?GHF8s=OIQLNC;DO{C4wO6b6bWwq7W-z)6i?x^I%)Z*2rpYtf7kAy1VGw+Q&JkugDF?-75CSIkABSqP0r>iFvUQYxfl#u8 z*Jd&w1PUM$T{4RxO$;hiHRc+2+MM|cfHgUO7Q_#z;|+S239{EC73CsM-)?rT;^Poh zo&p!9VcSD?da+P82IhBLPvi>!{>mL~2Aq*f1)h&WlYnV<-RJkB&!^NV1kQ8y&uxZN zMSwpawpS`{n*)H?`~C%g)vYBB9sFiSdS&@kBu8}o-#aiU_Lgm&<8wXWb$P5Py-$tY zri-DH?<%_1L}85nGvas?@UfKU3tW|%nC5%(io>>+4}z5u+UCL}==4FSeNL6Q->&zV}UR?r+MQF|*Gr+y)m2=y1VEg7_aIN51e zfh?rg0zTjY#57iD>|Pun8Cw|rQD6ue zf#EulfaT8&TX;VcX1Hs#&3)|PvURo%L)zK6RT{f_I)Bd~l>sCmAJfH15rdfECpQ)+ zdZGJnIy$v-3$C7)v?WjSNWs0uZe2Tb_&W~JJF69Xkn`+#l;M z_Rc%w!=l>=EBn_IsVXNgXG$93KYE)X6O3_Uw>Dnlta-0a;Q6>#+xmEGwjv346B)x> z1|dfeVE8-?#j{X#p6HYjKt7Tce7)X`DN$3F-M(|zN8hrH&MPR3%skQC%c7z^mwy2Z zA|ixRy4*U0;oA=Dp2A~ejQ5IzFv$7a4RjU>`L)5m4q4nFW`A#XocN=+|C+~)D&XYP zxvif)#KFQ?la9c6(M!Ea_uA293#lPmRdn|R)TKKjqyV}ZqhzB%uraS8nK9&#Hxof` zj#mUT7+GmvJsJ`82sk{T7tzZ6B$3%Z-EtkiP$IcY-wFjl%*F72h`h>P{II5HluIX4 zkk@`LcB?J)H<@cMc5#VETd_#Kh*}pRTcse+&7uVhr;%O_+27pCa{(U+R6x^)h~LDu z4}S#EDge9EHw<3HlWCBZUE4mF7XS2i4s)4gd@PHjhgo9jlu0Xd+`w;0A;2UwY5f#pT|8H(_!n@!#4 zuY%ofBz@V+qz=!1E#qv02thTOPAxl9P$#l4n)dluQvZ`KU@-{aOFxwN`_Cf|Z{cZs zmw%bumXE{4q75$)+0yf^o8H2c=L$UT`1|O4@iPDf-sxWeAOI?PDaOO8`ECOcGVX5a zPh_o6u0cA49he0;LWnudA;WOafC3@8{PH944iJ_>gOm{s4vTIb50^bqU<94vMDv@E4eAxLxOgg?DMW-S0 z$3*~}_&xb`RbrF;(k3_x7DiWw>bDb>rz_7QMekTfb2|V-MZRZWpgDd+@wI2s7}clbRNXt&C9#L!Bvf7-<^??pmYgryS2w0}bc%uy^WXx;@GaEf zzuiC(e%Svie!E|EzDPu68lMq30hlx==g)u3AggHAI&;P*4c|ZIPcqS(;bSq`nSB6a{Juc3i5Ny#oE`|2 zr)Lq6WywuzeeaKNfZyGz=wf6U6K>kuxh?Rk2c6of{o>VE)`z>_z)6NiuCH}>rmu$} zlXXfIIbX)2rVE>Agd8LAmvD#Y&pT7w&Z5U)bav;}84?UiEG5isBB_XW9e$CT$D_n2 z`H5mmk2b|l9t}tl1yBhIoCh1Z6iAKYlXfBxRWkjhTxYLLU#z0J@^!_4BrE5`?8x;{ zIw|inCJ8)etn~Na14$`l^c^Zq?X~QksSqxIgZlCN+{GDw`ecwL5SMiRdp#h&-(fLv zh}~unf(23|?sIs_^P}Z?y2x_8eavsN>-fTD0jJwT$+VX&wXMO40>3Kj-}+@d6WXW9 zK8ZD7*JjsI;5Zi?^Q506u)>Dj+aVq}Pv*0`M5N_<@2Z%yFZaGjj};w z9HV`oi<~@AQLM=n?ZSy}er{PZz{pS_0~PyPmgS7$RYg&bGFntuE~oRSrxg9?lOL6w zDZLC~;kgA!^@@JSE%X-Ha@%UpayYi1e?syB8`j8D$y(v5beWJFkwkWcc9rh5EqnO8 zJ-<^TI`QzQSx?nP{y?J4t-4=Zz6-W}8UFk!GyS(9&e#d2v>2S}EXROQs=J zF_vrNovlRrse6a}WjDW#Lzm5JD{qp*yQk+0Fj)72(Dhar5Gs);7j=3^*9}yVaDK|= zfO!NvDtYw8WMyvs95-gLH%ZK^a_gT2C!i(gx#M?sYht|A|KyddK!B!`+9CBkBpU+K zDTv=B#s#uO%bW$nu(5hLm;{e-K4MY$ycW=+w@AP4d){ejWk?eAs` z`{muA=6;`c1G^+bU#_LCWQ~AMMrQv@srutWF(6fl+p|oPo;(GKl)X^J)Rt_1ZkJ;; zU8*yTXYn=&YF-;XBk1D1e;LBRr}yzR`(utaa;F)&p<3mV@3p$Gk(e0K6{u_DH!7l^ zvE874M=p*CFsS}^%Vg4~x3qw!+7_RKmlN8}E z(+)`N#-L|}jLg)j+wmuT9GXU}JW zsyUYr7n}nmTlel^y8WJ6yS=TlKm?TjkqS>_A;PwxF)AKM05#N1=m(#B71Ecrtgu+7 z11Xk9N%W_b*+v1K*?u_@EL;cujrw9vKO~Hh$apCP^2^E3T`v?4BUttJ5GVYWUIt)D0fVg%4DA%1(e1#Gq5 zv3>%c2NCEjT^zSLe&%u7jb%eu%$sT*xfCAEgdkC{IVEe&_rX5r3LhdR$MP%{cJS|9 z560#xvY1miAX>yhc>fLK*~w7+)|(7l-ugXu4HK;)er$mN8yv$|!+hvnpRwz6vM$G)gA!XH30kRz&ZK6b(gc zyo(U`YdCzZzA=4t#|orOsNCm+RUx^ZQE)>w?qsvjI;Ok1X#P{a52c7y&U) zM8XeyY4(SyXXK%{WlBgtSflmPpLV6&1av;|jUv=`;XUc}FE2@=wStNekxVutU%(ed zC}6!}UKkokQSbTz@|xr~PAR?se})C*KH*ukO>j{!8U3)F*X5Ay!QcY|rQ_rjxG&07 z`+03>0$+t}q~jxwmLy+rTUU~#(+&q)9PVGcXArk@mYUU@^oASyPRbFrnfh$|JDic1 zDTOPs^=hF%El307v4$A+du+Yu=i(_fuJ_ra|AIqPKg6dUJw`B&F(-pU#C)T5P_4gxs85mOM(GHfKa6-Z%z&90}Dezbui^Ar} zd3ehH^@BX|pY@as|HTgvLTTn&pma^DC=be7Az!28a_ZBe9LO6N4*ld>!QqzF0gCW< zE@};86lN+xh6f(*$^M0-P|Ba^FR9dqkNT{0eflqTUAT##i*ues{X0E1jU>l)sI^O6 zWnNa0b5<6{7Jc4ZQvN1Zlr&C2TFNgUO9(Qiev=YczklR)Pv8*$3~974cq-ao19!s{~?TZFUIZLc%v=bOCy-H{h4#N653iXoLwWX7RHVH5~c zWt?pE9C`8}`>BuRwgfI`Ce(JI;7@;i$$Sq|BKbEq z21=XV7YITJhgs`cb+KB*mDuiL4O7qwJn^SU=iYCtvofFwQ;2E$I3fAWmxmPQf7K_s zY)YH!);jed#&1P)HEE#FeBGh_ zU1Ut*;SUf~5MkORtdJ)lwWae6c3T2MfK|>L(D_Q|+3L%+UCCfpS`w0p0W|-efj_Av z#OV=O?OMhV9h1|+YT_zJ-V#iK)tGvQ>vX|*kvV3PtkDSNY+XcYHHj^Iy>VHrRARdP z+eDMyIX6|w`I{h$k)1A>qm8qe=e6dO9pKoq{7JVO=Uw6#Js3)_b?yA2@>>v8SRo}G z?{^f7++ki6kxALdhdqp*Cu%nTh(_DTT^^PTZC$IZD> zVMgv_yu2T>Pw7Lr?F7DrIxDf-Tr#>RwUx#W=V9-lYF^Mfk2e}?+sTkmX6(q5iC?dW z`nP9zLCY>>tG;c9A&ysjf!s+PyTxh|dZUcQ1?}&Z%A-sz>RXE6E8k<}@?E$3OEr#M zfy$ADo6Ps5E;0&;R%hR+R#aSU^E{t1FUirTP8|`rOo^HITCy!Ok4hoIF6Ttx)z{_` zE3efu&~R%mYXv0IN`&Wx=#@*Xm1Ny{9m*_Qh{0z@V)9w14CBFM*U;hJEa!2)&p`5~ zNW?-T;-iio0%jY?$;wIm5{FxcjiBB9$VLRcyHxS&g8wyw1R%p*tWp+En^ese$uLv^ znwUL@mc)0fc*mMZV2zim7@r&7KdHRlUmul;fn#ec0 z99Ih|l%9BaN8#~5Z00$hCT?#1XD_om*&5M=%eW}JMyd$b%vz5>P&T>2w(LM28z zz_XGuecew@n9^X2;EK-}xvU~2=;o-4B#

5}#RqxVs7h`ZB?z;F&XNYU8t1n(S>v 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/SCSt-overview.png b/Greenwich.SR5/images/SCSt-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..46dd0c809e705714e5667946f7ac38c292adb402 GIT binary patch literal 96212 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�BUgAMG00cIrC8+4-QSzZYjb~M& zca)c+u*6~aslY8w&r0}sD+nbjJZ=|dNofX9Jac6zs5G1Tj&^`B<7W zy??t|?f@9Ef<-9URX`d1gB`S&K@Ohu5r(VIupJQBE|J6}?9z^lu<$#Y?F#WO-x%Wy zvv&-HJgfsuOVj?etS-$E_ow9!D~vjv{s;*8m|=K%GxT2ICZo=-xpaFQ%IpLXZe^jw zOGyA;>@rEN%0vjs@RT`wzK`PT$-PmNnzPHy_LWB-3g*CwL^;bE4{_5F||#}9%XmP z8#mnKw#lKTG^+e?U_WG+-oNCc=~Z@TY4C`SISZ45r`26j-74P4Mb$5kbxL2vH8Uy=M}|@Cpbfcy&&!SJ3N2sWdm;0N*smoBu+nCuD7dPY zSY=^9a0e8GE+Las#zqBASF+>9{oN~+PD#&afFPLc4dTF}AjqrC4k6zCUCLaEw9?ZN zP(cG>uFN@>l-Zum17g^wSFsfDo^{j0%52v@DJLug$wu5 zGz);mBTJ6CfKfg2;4FnoN48cO|8WSTxtUf;@?4(Hxmmd2-C-96Lq@ok>i$O0Xl`|l z0T5J>V@o*p!zuid?+S4uL31?6Il1;d!=-^%Rvx{qhxpgfYWIM!ZEzVG!Qi2;hMr zEqNfeXF|m2z(l!v`Ma0YZR?CN1C~8IH6IfPX`>kd;aF%#*)b7}=!Pw}gogO>Qo4lp zqb58W8-x>ty(0(etrTrc?k%~Hn^%B@fr6udMP7sOOP8boNn}=!*h?D)B5MxEX!{x9 ziVYYaBu1;wj8UB0dKdGk1Hhn+hcl!<1n#fikG-%ZVD+Ag-!G*Ohkgr2*;#DaZjOxX zxko!T`04ov!QBlb(gA2&+R3~xP0rhC56H{Brr1@Gkr~{xNF_ludF_+Y)A1_=J*M5A z$%v($A@Z}&K_1@@X#2jodBEt((I0WN=;of?N<{ZzKjD^)_D15<##K_*0FZGJJ#=ZPvK&i>xN zmUc!3(b2i1!w5))a>IJQ3b?YyBN-eDj=*I2Cs6!?Kk~*v-SGkVO;%@`Omc$|>@Y=gW~`45#51YR^drTPb*mdy>c%Blsp7vGD+Z6 zXJ7b#tz8AoU4Q-C=XdYayOm5eb62O z`8(281Ps6UcMG`Pc*c8l44J!eXBs1aPT0~G#-+XG>(B~=lmy+B-$p?r7E9i}J*uh% z3tMYD)&1)JgDjsIR;LRV<72@IyRbfS`qnVSOZh82nJhz|E6BO~d)HbcO8oB6wI@)j zry>mgyslAUY)`py;rX>Ocs6!=ePegX(f$$LGm@x;kN4fu2KYiR=|Q;b80{G8 zQP|5DTihM;kx7^s8o?;CNBot#Ql&|`=NFpqm32GpD!6Yl=rrf-LCBcfRA%#toByNZ znxiaeqFKkUG2g=DeDZe3LNXB@y%hYV(gBbrdO1KO+T{7edSdfBO;$2`4hyNVazS;; z9umH7>};8O4^L{m`Pv+I?Bu4e!yHU&D8i#}y<62QU7eI>$6dKK;$9hbS#mBz-u?ua z;m?3gd4N5T>5_1T3$=I4DlUwGGm*+}KJjv;$@n198%G%>TTJgKck z^_bJl69suRKZecRxBU|KBIMT=J=;e_I6bKhj{91@`lGTNZ6MH>h4!C1?@4OS?;Y`; zlWS#UHS<9}Y|gUR2=51JtHDPQ5TpwhMtl$F>jQi4eWl5 zB06nh(==+eafx*Qidf_cusV`CIz+BUev`D)p(3zgXAXR2YlQ2aZsodT%voCGtXkdasnb3v5&_HBF$p!MyZ}9W=8_)0u*HiFVXE z)z?lNbHicts8tl5Ctpolh3!F$G8Mo*Y9CZK0}%A6YP?n#fS}jS_X`8758;@n0No9! zC0z=H`Fp!g1#aSaAOB?hE z75vkiy2Zp-szh%ZlId_z;o$9x!8Np)3Fzj~3zjrVd)WC2JKvRfI+mZMs`~OXbVmSzZy~0oIdRzi+G6jULkAR z`uL9{qJ&lVxAHi%mas9>M`Hkiya3dxe{nFdJ=wHGcL6kI27%`dj)NlM)*|Uim2hjQ zYd5FRqls%B;zfgma&IU5(*Es>tHR+eK>1`{B?AZDA^diEvV|F=(qMN+PB?Ug+>i`4 zfqLE9h1;;D+el~ihk=SyjjYNWD>iX^R?x_ywR*8sO4})~sMkv7&^6aN0KO;Y`J9W+hjwMhJVv3=&d)#wh^;w7DqgSe$E!8 zM#lD^kG5Z6aj+*TU4Ix2^cVbe%%L4A;^H3FgkLbUYc$rjas@x_y3d7|!(GSoKQc;v zvKiaw+mCDsq5VT(7&>O^{YKwhUQeB4S`kT~cMl|%z5fk?e@ zw~?4yTub-VC6L=sACgH0jm^f|X_#Fe?_lsLRvX|LpDe1@U{qjh28o^r*Y^cqpQ>1Q ziJshvt2AJ2^f#VF*5+OEE7Y7tbN~rd!TlSViv97uSNE&Pc|N0#Ycl+W7X@5>p5C<7 zDo=-LFyYcBJo+z*opnHlPIStnll}cDJRF zCc)O{p)5Naa+#e8OHg(Wq*BvFUEo8BSKwp!u^#X>Spk{HNqN-d4Oq(yyF})qM~)7g z`8M^GXn2$)&(I-^PjRwOUy}N=&~dn#aFh=f5{h~p!($pGcTpGuMYatTfZka0A`Tv9 zeA^tf&t7@zsTnBMDx*7nbNoodOzv@E&48;~uv=1^^F0k+;Z`;}>JD8x99eqYJnFVK z4W~X7&74*r=!;IX&d;!|X*z-AH^jU#6=a;@(32p2Qo(F|`k1lt`ZnyJGtJb`1s`;O zqEtSe7$M-zEyM`*@;UAA8<-Kr2FcX92vQ@nV(!AsZ`u30f>qB1(2Vl8dU{d@Rt)ok z9~hT>Mf%AnOWJ7WP?a(PT&B!mGvqB~Teaqm2=MCwEfE}rZ zA$WNd79rX~wc~++YC#x%c*vbP0NRXY;!bS6*P7D%u*eq)>e)}eW`qzZXd`6yYr}j& zm4@;i*t<--IQ5td*$Z(~cmMe%%fYY9eyFI+gtY`JF8MIA)g(t-wTY+i0v%fP-ui8C zT(wMTcI&sdN9PW)SlTJ|lV5#p{Xv_&LoCol9Y4QTquI$FU|EwI)v4>F9D7vMPgC?Q z+p+o+pU!f;HSimd;bBW21qymkE#0S1LhQ#7Ro^+~u3AN_i)LR5XBQuRR!tWYXIG+g zA1voM)ab%13~P(Sr?Qi))ozHhy7BsCu$^NRP*7Z5Tb~W8mkQq8e1CzSc}|u*TOg<( zvB~mT{PMQ1#P1qa^PvXw(7n2Cc};rqj>!v55k_%4pN&hjzD%c-H*D(8kCiJEuFeN` zvslUV2iWg|jBf-rueBSGKBK!Zn|GM%xXJ-8ED=$SMYOodseaE7>Xm_f01BWV+&}@; z>OZZ#-*RO?2*CVg*GPD|N%o>%gz{4JuxkT`xJ67yFAE}Dfb4dpl(P(mXx;tt5LC&{ z=h_I zBl<0RQ+PCWBaSY{+$v$OOg0vSvAywiBB~%Cv7108wb5~kNJ+EYPmItTLOntV@60Rm z*3bskFiXjzScda_LyANS6OH2XD-V#M^N zmYo3svEQAh+|dRLKsH&hY_ypN2(~Fa58p8Ys6Y{oe2~;NfEn=ZGlOQ8j|u=YIL8i@ zYF(MDF`v^j@)iP=R`44GHXd0t$e8mY8XAC)!`F7is+-4bp5ZHwPp%G$lFZ8sY+QVL z#Wg_Cqa@>axlhfN>7&S}8mj2d8^qCbOm>ck6Cdu!?5M%yW)B&%XD*|9PFe_UJv6fv ziNds{QH*l@ghkhf;_y2r?OSpFW24jJ$Q!~)$>@SGJ7d{&gUbxV#y7#XxA!czr|TWtigiJ76^62Y&n&2ywKsHDK=*gp2bMro zGS!EnvE}f}F27O~iMo4`?f1BN3Ops@p|v~^u!G=4Hk*^p`xzZp4<;yDN4xUm2|z8Lwp9=>8!#&xUWD5 z*gtd>gv9E;{;PU7lBa@ipGSMX1)|-e5{|0_Dc<%TKQAyFTV9{O@U1%zkBCh^{`rxT z<#WgPaY>2IY6nX>#6Vx#L&$gLOyk>wwpzy%8wXTIPNgrzm?Y<GvLVmw)!&Z!ggjvaCBHE+hc;w4e4I#;VX&vsi%*(#TbAf8-?z~)sj&FeAYKn? zsFS&`Uuf7+c$QA(f8x)&n8_BhZgkK8EXT5B-QL8`v5jwLx8ispx28XH%y+)y?@j8X z$o-mDMfN(Z-Gji;H>)V4bC=KX(nQ?PZUz)!RiVkTE zp>^4YP=~h5n`^79wyPUUtCAkw<(sbfC4_i%?$UD&>v=4XDCnbm0QsU-j9Fi&IMq?! zyj62Np!zgq?X@JvgD=H|c>#MWuQI>X28);b>yEgp|~&GJC8`~-6DXG}fQkDgU; z5*Ol;D#_lEs#=~Qt=(yQ*e8ykQg#2NG&U3{{sMnWyy6bB%#*R?F>^?7GXwM-$7ve+ zTEsb*1vViEUw&lC@vF~##ku(oN`EKUWbi;NU$)AJbxIX=2gI;mM;W42H^M)COyA}{ zdY8RK>>7oN?UU0GkTFgcN*iVs6fIOku#w>vni!BWvLvxjg6>vlwQSw-`>sy39lhJd zmbv%{=u<_rg?;3?A-~V$!abV70F%4Qed7|eL;7$lN>ar?R)XwK^ZRzvhK{)KMR94xV@-cdUUP znku3hk)A0&o>t3PIHQ!}V%8j_?z>>As94DN(9>vUO?-smT> z)beTG*kE`>m7MDk2M6hriC7$v#pqevamUFoXHtS^->{%*`tf4xoNHHS-FfGqRZ7#J zTnQ7U1I1#)tGKe2dU=Tl57fT1`lS2Eixw5`RkL7XSe9hg!bTtJ&znA|H_>ho$Mhw# z>BqmHk6%*$&C4h9+UwTq!i|hrX7;MDEQyv z+zp^;HI2jf9?kjR0ZXw{oQDldl19Xz*lEbvr6=xu@bjff=ZUx)lHZF;Du?XJ8ZSu5 za^ow^glD7E8CimfcP`C~GzlV0_=2oBi>yJe)3g^kA!o>M<##qJYde<@P?HaqA187n@>&o{0k#DV*IT`AVy2)VFr_kwj}~TVatb33^KGo>7iBX5-<{Bqj5$xz^Xo{hs>#x)-5j z{&a7k_7A?h7%MdTK#dEwsV_;zhDBYkhscigJ;>xcs%w6!WMyK|1lsUjg0%PS!H%=2 zknH1nnz=iGKBYoq!}Xjecy$4}Ye%d9_D%m^0WHpzb55SH&fE7Zk%xJJ-o5UAi@JAd znz(P8cz92aRMl&fXDMF@6h#;khDhq$a|L?+n?T=;I=m;{?PO-IF5B-~W@_z=Ta3l= z(nrtmiAIlnRy%!heu`qnc%8NFFle1(XU(CKF}d}e96*-Pzv#M5 zd!2Uo(+SK-5yb=wr}--#`L6c;Fx`W9#hXDW`%3f8<2TTc%_r{c0Luzu6=r){1$>3J&crnoa%*nf1wI!8GxXUyj%x2 zA4Bw_u1(gr)V+i|AKFG0cE%|dijp||p$$jU>*ed~P#gg}51UPQy4-Xm?4>rrSv!d#-ip%bRJrM{A@BYr9tegWQy-rqcdX`_3bM^B zYVP!+-$H)1&39_n#;1*EeGmI$sjnX|z0mR;Zg^B#-ia&(qdOnkv?6g)fB zRA|B-uKl#E-o#C^azL$JJe}G}NSrVUln6>>7{6RNKUm(Ll`K>AB7*fY!^qd`Hc2wp zW|+z*%y@=TWYwXxUoeM`#hI6hAhFw5Rg2GDgbG;=Q>D$JaHkBHbO{~x=}WGO_3H{k z=E~>_0352mWNc(?d~)NlHfNO;LqBK`>z}!Ezn*gqsL3TMR9+OfG$}R63MP+Q7L+DU zSm&Ge6#5_kJYJudxp4IQG!?iqHeP5ho znr8XpQ}=vstQbmJvNX!P!Hze!uUxMF>S49HV1;rHWSa$SyqRjfFXIrC5YfR^Ba6*R zuCo4KxZBeTkiNfbfI2D|bU$WjS3$=Yj|Tb(fWcwP7RSgwK@tO!#u4%$PK{ZjaeSAI zIiVry(SqUH4*4R7TDz@@qxu2lKVLU~5KCPaqGQS{BREc1*5IRHizdd+xz) zXf1xtEN@1$ECOxSapn&P+M74&UJSEHV#`&z?q>58x6z4v3;5_251xUOkKqFXSk01xFx%qEcqd2mYO^Rf(!j^y4d19_T+PsoRo{IgZLw=U&5sC_VveX&cBidYNoRD-MRr(s+ zTD5fSLcei@;L4Gp~LF}4@*)(9G=u6L{a|E_OQm)k7tpx1&QiR%>})! zOV9PQ>f=ePX)|QJxOp z_0XKZqqaMbE)G$#+M~$|=1>cP!l1~O*^k^_%T{{Y*Jy?jIM~;y5~?9lTMPN4OleWL`DsZ~qek3uvouXNsghr^T;&pv1NlIph16jE^pa?oA8cb! zaiRJ}K#9-*=7Er{c_5k-X{=+{B1k)y4I+~Ai6K?aSK|0sAH^Q#4zUXONOf9t5i#$4 zEpSZm8eWP-oxOixt=O)-L#)BMhHHR!VTj|cMKrG|$ri^0Ar*=tYrn^k*hJ?ik0VP~ z64y=E-$3PiP5G=ONu!ocFzSU`oeWvhL}aLf=2}?TRZK}uf~I=WJ%3gqIoIh+LiI0y zKj9f1ZMmN$WA6QAS5%Ez?`)_8Q>r68#M*_|B01{U0>#iwH*6*sTK!=ZC)( z&I8s^gs)vUD5WV2ZBSMnY8y~n*~|A32*_Q7?q+3abublcCxnA{@=_nBoi+C@t`@*s zFgoL;(dRBWTPBVM`?#HxxmfAJ-;$r5co3DTPX5fk_H^d`jVB&?ilN+^r9+o2yl88W z?rHFz7WSCoHw2Z$;bjAX-W4?_toe&Y#>rccjLw~=?HbWyeinJhxL00#G^7FMhXt

^=LG!>?=FNiA^M8YE|vLq^yK$zg6!v)&xG; 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°DGR+bsvb*2Ql|1)1Th9#fwhk@@7 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/background.png b/Greenwich.SR5/images/background.png new file mode 100644 index 0000000000000000000000000000000000000000..15dca6fbe2669fae3609605e49c69cc414f1b6ed GIT binary patch literal 18255 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 EKU&lfD*ylh literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/callouts/1.png b/Greenwich.SR5/images/callouts/1.png new file mode 100644 index 0000000000000000000000000000000000000000..7d473430b7bec514f7de12f5769fe7c5859e8c5d GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQC}X^4DKU-G|w_t}fLBA)Suv#nrW z!^h2QnY_`l!BOq-UXEX{m2up>JTQkX)2m zTvF+fTUlI^nXH#utd~++ke^qgmzgTe~DWM4ffP81J literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/callouts/2.png b/Greenwich.SR5/images/callouts/2.png new file mode 100644 index 0000000000000000000000000000000000000000..5d09341b2f6d2ea2d1d5dad5d980f14b4b05dfd2 GIT binary patch literal 353 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 Puw?Hj-p-PI`sRNClkEZS literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/dependencies.png b/Greenwich.SR5/images/dependencies.png new file mode 100644 index 0000000000000000000000000000000000000000..33c4e7c488bbd1b5e1f8d30ef4ef19bc1afbeca8 GIT binary patch literal 24825 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ðKaSt6${Em8qL^}$*2L(~nn@pQ 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 EH$Jim6#xJL literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/important.png b/Greenwich.SR5/images/important.png new file mode 100644 index 0000000000000000000000000000000000000000..ec54df65cee2de118b8865a16c1b5d757e33709d GIT binary patch literal 2085 zcmbVNdr%Yk9ZyB)kUM)fy*ZWlX7hkR9=mxg$&&EMiv$RUB!mDWS(1gskWEOIB!Cwl zf?{PTT2XP%yB;V#^%U9;pe+^T>{&UE>x@@VD|&aWW35hWduOkAqaC|ZvHiolKf1HK zzu)h>-_Pg!p50|ED_WP3lt81=*6DR>6SZ!PJ@IkW`;%iIE>KG%sj-n}UjrG&0ywSE z>8r;9y%%f5O*rOkZN7-hX|y<(+hQYahEmkw^YXEn4nN}cQ)n7Zo*(gJ4i8QO^?0M3 zP=NP-H46f6rvj{$7$AdRg}dCkwg7H!E3-J-JPw%?%+CYl5tJhE;v@z{yiG(9jVQp! zyePGgi3K3=ScUW`z$Z@G3`RiZ3*dl+FXA~M7zPl84~r!T0&@W&1PcWabt61jj7ktx zm;*e$K+0Oc*?^kV+NZXtlLB;+q#qRs!r?GKEaLkDjRIIElf^iMLLQ~T3$_v@7U2;= z#tMTP4>|&FKk4=nK#UQq_qC7;kn;3N2wuOz@Qj!UK1~#rGC>6M3t&DZ@Ooo$J=PAA zCj7r{JXbqtY4zg*6CU)n1RPX78W<~JDtF&)D5gkxgKi4AsiI&_YM-OUixZ??tpKSn ze5c!qLLw=Z#T+q|BZLqs3`%u1gPQQ^_OJRXsZqwOD&qLO2*a!%fyU`U&AilhSE!u zf#RfW8Nca8?LYcmzi;^J0$aTLuk(_I7B(1E%i{iHi|z|Ja9*KR}4%unPJ zFw4TowlS1#GO3H7Q31*c7>im^52SWUc{QwoqtQYKQqqoI_}z^Db(y?bEU3*;g(Uk< zbhQt9Q;Rl4_Xd*GuUR{_5VHeEE0C#yNL!dhWt>(;lnbF3j@_RUxGA zhlU&%fA8^*!l1Y?gk+ci-WE<{Z}q7&M>qEshlgBmoET)9!8{*KHv&6`TU&?mta6qd z7iwD&9iFFcM~&TiU^y@_(iItM%&Y+Q4fzTJHodO2br<#Qk8o=Fh6?xiG;t(<^tVlGN*YwHYbN*+ux#qerwpu9`;s z-h^IVXo>ux{&d`$r9Z!%mi_6zmY=<_(Aa4VWq+kPR9x~xOWlpzJxnYGn>;_NtFFtp z54GGsQk4p=t-Lq$;+whBb8|*17xjJKQ38{*G>h8VSmBGr5-Z@b}+_3*Xjg7`HBiDzyy{&6?adFeNk#BLg0d5b-3 z9p!F+xWNDCwRfkhhF=kO!^16Ky!0x2slrhor)q_mdPk(;+PiMET zz5h+ansg!r=$v-@J7+7{oa2j2pl#+KRU%es&<_a|W z!QKDvpGsto{Bi1?F{rbP{YmvHRmJgSd->g=lhdE>DT$9i&DZ~hSKGgD<3Nr~x0crR x@l@~8v%fudb7|Fs)}6WGzYSl#_Wjpr@eu7sVJhKCFm=a%+M#HR literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/kibana.png b/Greenwich.SR5/images/kibana.png new file mode 100644 index 0000000000000000000000000000000000000000..571e7afe8e42b1e68a4ac48c8b0573043b72c0f9 GIT binary patch literal 186956 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/logo.png b/Greenwich.SR5/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ade2ce6ed9d9e9f2f4d9c5729a252ee618a0a5a7 GIT binary patch literal 4387 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 XFPXHi0zU=*VW49SDZKg56Ndi*r<2?j literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/part-bindings.png b/Greenwich.SR5/images/part-bindings.png new file mode 100644 index 0000000000000000000000000000000000000000..da8d8612d6913bbb4044ac167585c9a9b7593e28 GIT binary patch literal 61765 zcmZ^~1yo(h5-tn`cXuZQcejH>aDuzLySuwv@Zc7Ly9IZ5cPF^pf0%pc&aD5v$6Bxt zdv|qpS5=pMwNJRbtT+NJE-VNL2!g~{5d{zsFgD=lD`-gI-+EKlb`TJFbaP>0c?n@* zVtEH!6LTwL5Rk9o$*NFlibI&$nhMY`bjYAoKU@pGrmjPP9_f*NCV?Rr5|XdZkA$s+ zz`&^bK||b74^p#OMMJvy{0RfYSetfH-aQelrQ@#s+S~D9;=AMci|L={L>v$$0=4Gm zr4n2au5PbyR%Re9T;$0T$B@{QpN6|Y(yA7>wcgXxzJeGxy|=b?!XTUbj>+Pl=nlM> zmpC>Vu!DSs?>)k2;(^*02H9Lb)%pei31Tnd_#U4_)Ax#O89^)v64r!%y{o1fF|)U* zVuD752VG4BVniKDz73j7x;I9vcddtyK_w1FB2FCC1ZzOVp3~?50O)Pt;+i2*As4p# z{w9pSzF|-l+iO(qMy8e>DLp6%W^co8Ise{_|E}p!L*CQcMoGp(Th#sDT%H`^z85cw{B2vLCwC??^ATJ+5Bpw|%EUjKOOdLJkTNhNnT`QyPqwOSl%1 z!-7W5Csy%q63)cH`f=f{Hi#63+khfrP?#x0;*&}O8=kzPx6HM@)yC+_F2#;j0Wr#u zLqzoDzAvG9dv{PD5#L6>D)UqS7AzY2Dx;b3<6vlT~n*hCSse# zdkX+9<0H3TEVKHKA-ZGx#8_;2s$Ht(#X83^e>dCuf3TtU;OB?=|tkSpJJD`_T%c|2>m>T-{N@2yPW+A z0GKA*vCrMBTDf=PAqglp8w*zlj|COJqvAhQt5xj3RedFLXYnUf;TMN_(U0b=ihWo@ z)r=tM9oMI*hEs!^hfZ6xuYj@aiIKegO3oXv+J>{?+K_Q9F{A^^~LC%|l2(C)-q80$ZFKK0JM`m&y?y zP(N`PrK^q;wJ#v}?yw-QudZ1`cxr#jK{%Ixr-Z@oav!S3rsQFmch5zGegZGxDA(`|m{groOI zwEMIg=sfjFC&1@|5S|oTNXQJDU`h}kg{2qLUT77C=reY>h(SF1XM`;hA(4PlK}?du zuQ&?+R>H?|8}Vd=61!m2;j4lz@jAN*S2%t0)Meqv`TK|1vjn%|WRq@Au*1UFlR{2R zEnsVYD*3HbkT;(_VA}m%elSdd!s`uVe`4uAWpt%Qj_g;chL;W+>yfN-sK8x`Wb3*2 zLOAJus*>YGDc+G@MVvRm&i1RlVt6J|9oi+jw=|eCw%ca2rU7I zw;NuF=|^T&qFtIDqz?yQ(7gz0ykH%1%kOjM;53O$87Ek`yNd?~*!jxnn(IKT<#9xXVk(3`q|u zQfY+evr3s4P9qkY6w*lYNd}@jM?mytpvnyu&=y$bo)%{QSS@Z-R#9PBxs%T>p_SLk zZ4-3wymH^O*^^U3C}U%b-+uUP}IRG0un`6BITgozTy<|O^MdK{w z?9p+|@zgQb@yMKOsjj?lrQkH<0`&Z*8SVUHrM=`16G48)hOKkSR{I$QJ2 zQ(T+iR?xGqA%|VHRm3yr4&iKqTRKZGi#BVHJDMBG$zI!XExd)riQ^=D>J0BE*Rmz6 zr_<2kr~LEdA0G1_)gHCC4JY+S2#0uwp~nZuwzK?&8>U^17c7IUYL<4RxeIYNhlZ4l zT9$6B7Z8e(M-!taHhS46cX&rQ!8j684LJ392)LTq6xdie=PW*YLQM7?LyYQ{(-zaV z7G~VPonp+}kF;6HSzK8T)k8z&o}8!78b8k=k-YH#XCYbzSs zY((oE8i#9^jpp>DHColKf71N?wV2CZ$qtqw!=BXuu8E}i!@B;Aj;AcEQ`=v=UAvL5 z2Qb|+*fI4shdskoYN!&+#j&3e7%AM)gBa==gB!C$5Q(2bu)(R$Q=LVfRmn4xDV%x0 z!@<=_Xvvbo<8r;YLhqCcLocPJq21Ba(j1|6-*VUTy``{Oxp~cD!70{W_m9td;3mo@ z(?)*ZPGWJnX~zNRL}%M+TTPpbJ0FoLQw)#IK-0i_|2plt^}04dI)^pqCqTPHtb=yL zVnf{Liw}Nhme0h?&li?g*XK@HJ7{DGe8dOjLmWo6N?w+M2(RdB(l3};E^Ab&A%=uMU^xN9 z0kJHorudnq07cGP>y+CH44n_gMU zqhpiKG0(_zME2XWqP6rFP`ZB84 zR%b`tjV&$O9<3_#`L_vA>uZUwuBR}|Gj$6#=MCOb-i6PGQWMbtW~|<;y-;aF=w(US>^ZexWjdO>oURcAId`bJXYM zD*Rz6HM>vFTy7)>2T<<$=ccWYqo8r8uKbd+Ue{<(S z{o(Co#-6S7`g|MSbEPw}$>!E`=6xWMB;eSq;&u1!X!W3AVgl0Ci^O~BV(!@Tx-rc5 zKGY#3#6g`j<&@IzcZi7*$VCw(hz2Y#j}wT%xA={AqkzHlNM`Uiw3LkrqitA$Xd)08 zIFPU))1}Dn> z7YO1`2|#3bht9neNV>0IRgT(Wwb<3cd{TVDL}@K=e@ZZ=0LugzyRYhwARw6JAHSdy z3ZE~4RRn~&lA4p6j5N2Qtu?*Ak*$F-y_>Zia5M-AuNyb;qqVVtJNUtso-yUvl7oe57Vh zPIlZ346d%O^sX%QwhpEYOk7-C42;YS%*=Ga5p<63Hct9(bT*D;|C!|Pc|?pI4IRww zoXl-)h(G4lH?Vbf;v*&fVD!JA|E$y4&HNuuHje+@7I1?MA5R#V=ouOQZ)8s9CjSqz zk0<{j`)gnSVaNM18MnN-o3WL;h`F_~jU#Yr09H;`-oM!VA5Z>S=)Wk{{-I=IVqyF@ z>AxQRi}YhD+;R@)#y}~3$O6E`%kY2P`|o&Oh7UpgOK|_u%3pVZY5~CVGW<`)0N7(; zkYErH0T2lhK_xfPQ*9_O^uGJ|b#3}~1tGR1VgjlFzjk716`#evkail(w}^;5O37LS z;jR!2+L98b>Td-cNA2Ly5?|Z3mfk&UIfnKf1cXq(c4v4wwDMf~-rtb`6fQAMO@FOPv>mnqOgnI=2zkiA4>5!2Ni!@mCm4yDG z6e*1d{Ac)IRC8edaa|g_2M5Alp`X6RTmHkO>g1+^z!xYxhflY)zsQlUr-eg0>W$proeG2O`$dN~utz12%roA3EB z!MvxvpD5b^9@*k#p}Q#maEcJ!Y@%ZGmxR^D3yDoa>`~G?s~C3UvuWa6pQd4F z=OHESMh_r#1+*2Ai6YA(ijOJor4apV6IRfRV4bhK4$T3>YUz!6U>GUe@CZ$7HVOvv zxh0Igtv%rvj1+A;3~hh#E7Y4|kKA=wWo*JjoY6L6i{LF`MIZ!TwJZ0)52t22#;ITa?w{9^u7mkSao7 z?d?s^dq?E>BcqQXGehv)Z5XHMOCmvdn(Zp=;dfoE7TUxTndTZ9cC{3S+F)|)Cos5+6bKUy_$-jzpH%> zAi$8@F2}&C%m5tZk1@o?ESP)I3I3ygYC?ZhDz*w%BU95 zkQ}n+Yv|6F0|p~0V-L2MPA}EwNFC3I)~sPA;XnA_L+bDuH0mWuPokj!J>}?lDt=z; zUPi{v$U+D4Tl(ndDiJX=I00>$v~CjW%^)*}-&ojT27@|iX|l^PL^?!_IHBr^_0M@*T&*fkuB=u_() zP-f1iODNIeNO`@a0lYZDDk#jLD7ONR7h-;oui-T9RYKY8EU2h8o)SrCztFuscoND`# zl3BYcEA>+4T1;-|Z$k4@ypC{Cok_sf!elM9Z^YcDoowZds2_P<3RnL z4#9Sn+bZXkFQ?K&B4IP(hYudTMV~TBb|-}0Nl`5fp0Dbg&X!UyZg+n%T{Kw6wJt!0 z@_hQP4psf~3rCG;17yu}!%x3Qr+<@zNJN2-B2Ey%QAX&}yCBU?I??uui0y7}#5CN` ztzeOoOV6F<0}+CRC&ml4RiPpiCe9ZW97<|9fmopia>H4SVKHA8zU(a5eS44iJ1vwU zbVh$N;rpUFG%d{EZc!kZOrEqbq)=}pQb%K(jDgKQ(|IU-C)Vi4H#SlPvfiVv)6P4? z%}Jyx#|7EZ40afT47Of8VWPg&+V|iEu@0I#bci_HsNr6IuaxMACbOVrg0BUIxer>O zxH|B6TOAd&cmWBCB+G#& zXL5ZK;Y`D^Uow+vIk$#KgxgKd>=tV8_4-|Nb}HZAg;h0h@$Alb9vqK6;-q5#6@oa$ zpp(=}_1)_XW_}1m6>fsJ(wD9d$8!!4kH}ij{!%3xl1cnMEcD!8;1Gv|q{)dtytMc< zpIl1rb~I;$c*;A<_ zx~n3q^)QSWB>yKm3Iqe4hxk_4bUkU$c-P7NPhntug+tF2SQgf1iOsB#uH#|D-`T(m zC+ZCUeHU#xgyKq+VhyTev7ScDq+@ijj~eL>F;651XxA3`E&j;GnB&AP7UvsFMvEV< z>5^NH|H5|*7d5yhETw2!ZKa62nb%mTCY2WlbVeIw*HNQmAsH2-6peo(La>8!ioIig zmcNIP-4m0dp@oYhs@n_M>z%-Ax%acV-MqR^6-f|$?iDkp?qt{zsT+QbzUM>_LoH0Enb>ep6?mC2*b zNG6-znY5dooCo7&)=bwe_rvuiP+c#~%pXR+Os6HHdoG8BzG~Ul;id>4fkVWm`(oBD zhRXr!56=BWoSP205`h|O7tgHJe1deQee)%O*{V+6cnpBxy>iu^B%6cn;en9N6wt<8 z6Y#kh7hynNYpr9DmmH)!7R1!tu$~-0yG_y7yH)WlwsUg60RqJtoj$(vNd#^r8N#D8*5py6%05!)%nbPotm?x+?C z+DcEEV!|RY4TBY=RLA7VE7bfwCowcQF|ni!2=Q-vcbs8^5*RU3`@cP)AF*8~%BJ^z z1o$Ns6X6YlBIonfK(RtLR$3CbkJb1{sxj`9p2AL20fT~k$B-<PZgebr@!A`pW{`q2m!sb<@&`|R|a564Sjfch@cd5O9Au0xpAscs|E`X4>z026Qy#1Ed@#(-xip* zbFI@6@8zUHUyP^RguAMkEehEkmll_iBb}94-2UQX0_DD|Z!nmU@{h!%Q@~^?TzptL zS(G9t2g+H%9GUVaibjV12L?E}f4-Q2Om@NKMFN_pSp_Tt4NKl6K3kpfa7p#Soqi6k z4VDd&4TXw8{j_3{(C1npdnHP+yjdm6wr(wC$`}y2Y%3T&B3vDZZI~nq>2D?xhR(bO zO$2oYJ!d^;f^hEOKuo?n*PJGp6pT#2xN zEBzr3rWI`qZ+w(ZnOira^u_lE69tQ-k4CfY*_4a$+#_r67Zv7o&oa8wCiE z3kW2oOHM+Q1`x}C0Y4loB~wRn%ShfKSRA_h>Og3VQ|R?!Wy{cmjX7m5&*3)#AfCUv zW#2>LLgo0h#vLTDJ8S4wVja2_7eA~?9=ulp4IMKa9h@2oOy}yru66D0-D~&YdE)wd ziyTc_Rs(i_OgGFb);t!*>uga*wzAp%ap0$rB%wmf^qti}8^p*amZDB9z=n<#0`9}0 zt7nxq8j{nM?ZIcqUqYZ1*Rg{RifF~lTbQ6>qJ^4zA_KYTjsEDyHt-St=EGv^_ACF> z&rq&PM$O&pb(WUuVTPy0^WDtE>xowEm$kpH*eWDcA8yq(WHW?+a*Q5klEI}&vC*^& zNs9^1w`x1+mNUz@KCbqT9{Qh1cE{6|pg_vpIr~R-Sz}p+N#B`9cz-X?B)^_K`zu3kV zCS4Lc7$sx2(2hsibLhg}X4mWu4dB((E|2Fc2WGph&{y(C91 zImoMsT$wb>9oWF`6;f_>AUHufiVElciz1lryXL0x|;C*>j@W8@Rmo}x2_^`KJNXnyMM zC9hfcJir06;0)akJuE zU~+tK03EMJq%NMQjCPZHT1(GPg+Zh5#-uhrr_Oa|(;((c04?7G@57CA0}&p7cYg1P z&IS*ZgZ1Fc9IrrJy4KC3;vB*Ic?zmN46)c(?7J{K5q7iKVko{SOax2J*?JIZE+EF>PaW;&lU6>RjJyTme;nnf|FCI% z0zpU6JlNQO;1b{Gzi*KvuAK1`^V#&>FZlhehfi3{a5s@->yPEL-dh9#wfBl|r~VW7 z6D3)&Za>oRMo){)vFdY!*>Z*-&UJ7rF2&`n>@8IRc`3%cP?!6T_XfglJW6wtaUgggd=Cm_aOBADsAg*rb?(KpZ3_>D zT=lEt-{{)+Hfi6Ndq(GoeZmPGXbZh0iV;5FXJ5U%$>98YK}9Z`Ow13A=kf%|$Wk)S zW}>CS^)j0bsFf<%n#kx_yCwL`Y@^U;Jz@EHr=%y7}{0gw0ICq3jS6e_e8>~Wj__4A) z8~Wa)6`A~b@Q~VGKmF-=Q|K#)wCOI;%?^$%Yqh;~@M^h5t$4l!u!iYNr|-P1FAz@% zej$YIjPXQR(0v9+h0b_A;xd~n`otdNgPU;&h(Wx~@72hcPaqP^_4r7OZq39?`@*9@ z)YUS_e;EjCDX|IwC`}j#k^1Y7Z*VVoeS9wjwH9~yb_-kW?`MSV7ouOy zx(^tF(b(P-^WsvnBU`5Q(;jz<({>zwS0%!PREVfL+e)^9gt8F2u3j^GWUpdK#K!a~ zW4S1ZHX{|0u{B`e&Q==gB%ViQz1o`LM7$aZ2}?8^l%r9}6Dnjs7{(()AqhXGae6@8 zcsp@I0&c-%&Evt5+V}Q09jG3>bgsroaE>xDczNdN=;>4NbNokFot#bLCwl$ko)$X~8KSg1(_Xy1?2rR3ZQ5{{WD_|`!$c@Pl9RGeAI>>cC5byz z02+KFG34fxA{EK!!F3^zPd`T}G5mg1Co0knfZb-e_Vw!S8*XG1EH=0!{NZV*NN3ZG zqM11Vx|=xwKZQcFh?fxd48<3_&qn6N8XqT??^Ng|{rm_udpLvjd?5GR_IjePZ%yMj z3QqLl?Aq4}(i1p~z$|Y#FdCc!u4;UIHPTobxyNNAYt2IR^v;EI;d_&Mr53|8q(Qsd zJh3_@L1DVzrBZ!?bV{`LMrN!kl{&$*TAyy0z(q3GD!o+B4>R!#Tz?Av($wjS_HIf0 z{@QuU)9YWI_>qXteFA0URgNwDN(b(L9P2l6ASMftxv>DSQyA+o4-bvkD`kJV&CCQN z?>ZPGTY*sL8dPBRnT39-HbOA4oELZ4DGJNiLagyS%zZIG%(HQjy0K}&V2JiW%5XUo zHR%VX&3r`zygd>bFQh%swI04tc4Bi}lC`}IVl)*z7pgVCf@CsDH0`(4+D%>OS9{FJ z8sRTSo&%mh;8d-|)QmV0PwR&zJWqFx${VvXZ%i=CnRyy*i+P`!Z1)@8&I*TiN39UQ z0T|B4dNQsK1)TTEshJ6X*q6^ARyo6lM0;UfV~!7q+qOq|Ze)fq^31GE9b`k<&-BIW z)6el-Zwgvzcj6OyKajYXW_aE|IOC0}$9y=AV3aCeG370du)}~Zm;eDc5g89fs)%Tm zg2l8iA;_lKxE$6ZQp`Syp9r=ySVhg!jucS|m7y+gEI1jKdbzLd7W^`%3bkoY*XwW& z+E`!?vP_;j(qg!+q?tCuWP4}MUt_Atbh>|X$cWE8T|OL4^kD;zGz%tIYI#&}q&rC*bKjujqA)|I(kder2q7)?~GuAv{=}BHEJG4%H|z55lag2|UhR&ExW_ z!`zoP3%D##F<4z}yj+c}wVSI~-JU9^-)5r`_4Vm&y~yG~e|H`D`PF6)%#-~+)TQ0F zA4A{A$7c~XZ6y376;=ngV)aue^ewaE6R{J_9VYJ5^~&b=1+=)FN&1i$dRA_>tF#r2 z22H;)b6+IHtq3RboOK$EPlPOctDWz4yStGA+R=B1$3?_U*oC_DzLEKR*-~Cm>S>2; z=?kh`50WPMZ2B!Le&J?EL@A(bHqXH(8tU@gLO1%%jYap!`85Q2Z^SUp`GC0n=bosQ z7yg-y@XA}fZtOBe2zoLY*N|<$EH>xM8^8JUUAHLWWWD7=KvqgBuTo6nRl0MoxATii zA*r)U3UwLM<4KUhBbPako$x_rljrpLjif7!@$B}M12YmS4G+@Rt}QwqTIsQNLQYMc zs>B0aG;sS#L}0;WhL4x%vL99>2+e6m*4o`iT8oUwH4_q|VJLw@JfKeib{!R4@9Bslw#()Zb!P36^GlHox*d27V-upCG0~SRhfIf0FByl+Buf*^y979B zaBr!99c?UN!=uc0MRE+>KNngpy0)TCjgEC5ez~*AFWdYlID{%_wNFUsxN1=Nd-v|o z>%*GBZ|ZGBz`)z{mk6e`f#eSA5Jwn z1(mCu08C{>u)chisS?@Hyid`aqO1h;sD!-ciqGOp!UyCU0|Ok3kCs-5ESQT7>H`-} z5TspKnlpa3hT7bc9DfG!f0NvY4yC-a4CPL6Te*_A0WMm4;jjRwg5R{~ce zRzm+vxkH+SP7M^0xCpRTA`|)y9w6{Y+J#osvnAl*z^qcCCP)-^6Jem(ft(tip<>pg z-Q?Yr)zmI>NsCsIppMM>1z7AmO6#Qvf%^-A&&~kOR8@cJ@O}RB#Q<0Zi=yLw=Y^~0 zPBD#YRsON%vURuRvTYsSC+SG5DLjk{9)k$_DBV4^^`#ly-$D5QS64gi&_n?T=r|a_ zA<(eWY1DCYe@T$~5I5Y1xUm)s6xUy%$50(Ou`+S^rmp`ctfk;4HZid+Kf$NCLTJI< z0%)aD4y!?|;J}6C;IkFi?i~(V9389%o23ZaRGcje8Y=LCTf7N;(qUOHb$Z|Kxg_v4>mc1v9~AzF+*Q)D^WE} zN={LOy}!Di{t-Z9OpoYOZGr=%00-+!O{-hl4=U+UQUI!j`r{NRC4?X|{euSOT76(a z>@S}ifj;QJ;FJ<7P%fpP5l9UFg;p(nl=@Z=5;p&8H-7%0mB1D1{a>dR+`r~S1xpu+ ztH3S&cgBBU0upE?7jXEeE|VqQS@uKWEaL-(rE1hi;Bqz}fVtw>?VGAZX|V6uLBhC=<~7V zyks9$cochLHuKXJOO?|jbyFMp5C!XBt4Trx`WVU3RC%CqFu{Ckqqc^-4neaRJ-U^Y zeO0Wy$k~>#Z|ZHv1-3$A@~7;(%#4(NGq{z*4#s}OCkt>P>Ut;Ak4`YYF8EiRZz|sh zMc{L4&`sHo{lpO7e*a8$BI7}zq9KPiTKPuQn91P`_O#B2L(?!WXi(7J<)H|MDMb1q zWP$JxVTv%q#1q?Xlj`jllR5nPnLJC_NuL$oPtcM--hF#G)2tpYU>}Dx7UJtGc0U5C zH-GihEJF$I)M*sJ{U3CkQV!fmiAs(&IF(pl21V~cN5{ru_RWj-I{*gD)!e@iPf^L* z8^Ew+0G8#ej>1gG<;hOpJ)}+FUw|WJuhaVZn^~DTQM?5VSTg8GKUg0 zWBWdp`=$x8(}x+im>tiL;D}HpB=q;53s$NBz?qPm$mi7~9y_VpaqR0QCoNY&&pZ-D zgCA4@$sh2hz~hGnJnBLnsj?G%YJHS<<06f`qh2({V0bNWYej8c_;+vCA9@sa_#fOK|`3Set1A0W( zv#wq4xB%}nm|3#ukRv0kEyG1lg(PA-FfswDT?m>j3PyMZXP_l>;({kZgwV3KHrR@Y z{7j5PMu~V?bvpezCOQ*xyDL+i52}0xlxWvCAb2a7F0C;z1;+<9z98@GzbRvy&5&<1&-qUeD~9~TA4}C0(*Eo4GJ^MhYwFI$ z=>O-S+R=VUTP=^y_pyz=NRKfnI zh22&C5wi@#8AIe=eCmNX(L*(xS0H0sL+ zRb6Q$-L`0elVSw=Et~}RY?WZ|cPk@v!h=oUBYu(D|!W4R(H4j7zS;Xjg zKsF~Ezmp6GW`S=K8~Tnw18((X5{B{+1M~-iT3=rw$1@<|Mb!XHM(q7-H=05XV?a`0 z=t*2%n5nR+0-n6{AISja^^sJ?KpiXmtGSTG12l#to`P=qkC4CvhEdNu;PptS@X@rj z^ZKx*hfVrIU;yqyL8fHrBf}>0S;<@dM8!I)s#8PhYDB}u=%J1btx;&D$UGe=6W0mq%I zu_MT^=-3@tFZUugHiSY#J)7`UIS1U?i%pqPNd#&g_E_Amdn+3%-wHz(%G84`$ekZor{-W2$~19ud{xr;Em6C>yY242uTAZZvP_=j=^UWIN1At} ziN4jEvC^ytG0mC6Fz0ntyr?EMhDnzdhtN5BICf!O;X>NcZ6eyzFE)HjUn`677-c@u zycyH%{{qsX(5=AQsefVf7))ESSY890INOt(oRR}gr>_5yioFKqx)q27{N@U4qr!Ar z5jKNwW^#L`Dn6J&X(^{wESKqP?_^%jquR~6L2qxL-eYEt-5vpw;eNMq36Xy2h^ctb zgFJagw2gymM}|v|bHc6I&5Gf=o75XFwiEpFuTVowFBiPFg}0Dvp8D-(7#CCg zqL*8&V-Cxb;e+tGEyibGz@=*t=azh%n?l_qofu%Zl@D4U4)q{U2u@1TYBn~of?vSX zj4WCk<{&tAq)%od#~u3{xduY;=Vzb6u3r+kWGQ#H$HshRrdAabGOV|X8rtE z@#%)Q(1mPY-SCUo)!@VVW9DV7=sA!K1yrk59ZdOKxBV|Pgl_Mw-La(@MNUQ=bM&U9{>B z{E;q0qe8}id-A~-lS>O&>hubYToK(qE4_x1;^^?XC)X&06+UqE*lggj>8V1=>5{qm zGb@Ar(#x_KcdYOz@!Hd9%O5(_$)KPC z{+%l4x#*wJ#G@2g^RvC7Q|}LhbulY~Xr4iIh`g{85|(onTg94uHW{s!GJZax(#WGy zV}pV<6Hj8E^k40bAOIvwTA(M+jXT){|f($jnJQKY=NjCKw;Kdyv5HAND~EDN8)G`gyZDPH ztE=Gs!aW;|Z`t>aOukdTAvB(;dGE$k^vUmQ{(Q5y55J0VMp8yO2;0!b$~DDtbR@@? z+cgT;-iJ92Zb83B*7Bx5Zl^Xle{acdcFM@{-Rq+HQl#(;B; z#lX?w?%LF%)0#8TM{9WDOb`t^5yR$kCdJ}nrDk%I_jbL^;Y+Q<-}auC=WYF>+J9vn z8J$Yl)+=z*8ZC`$kSdo`Fb#P0X}BE7Z0?WAYK^uPTi55TB(kblYwKC8@~CF=8q_l! zpC*V%CDaTwEc`0Fm~+(~G-eF(qb&`SF)48;#E;NwOt7K4nj$qB-_cK<%@cMltQg1B z0k-e?BAl?_xu=tn%xhmjJB`{@8N$EJ1u27X+B%BJtA48FV6Hz5!jkY$@+0Bwuot~~( zhNp7-;C(3TZEOANL)KB>NA5~)IR2cPM1AL#%DSGYQP)*?bS*jQ^u@I#zGNqx;peyp z=dH5j*;)BcqukOlaT8BOnt-cg_Hude6o}av6F5NDvHGa9gaqcgY!HaU$ZCe)rw)uXQp0As(a$3}IALL|3U`4Ibdi6u*i#ebf zj=cpS8bq}hH&4C#qx3fMg0k&2$&dj7blB{M4Z)p(`ss zL^MpyKuD%#|8XGXumn5Vg%PhrW{pjgPSGA$aWaQyq4N-Qx9MtY^qRr26odNHBUkgj zB+8U0AAOJo1$K+eVmI=)g&Up`sg3Bn*9j5yWWfly(+=LIni6?(C<3s6te2UFv$U96 z*_@a`pNXwuoq`zqi{|)bNhysCcKy>dl)+&~vP`ewH`mv!5R4z!%McU%iD>$J{8+ZSeMICg|K z!Z9?mEf-_5QXlO@7%F};3^-D}JbeT0;Q=XIm)*S<*p%C*LdR>kl{mb_)0vkzS*ls? z-9~gw%rdXVuBZP10igNr261D)@g{~P} zuL%D}-B9o=bE8WmH{~j&#n?Z>7%F~@fOY5#>%$v3{q>Y;eV9R=HrQrXmEK2!AQlC7 z;${`k7ILn`m4zG`L|~Cbswb?|Z*$Xj3Z}Jq?c|i+2wtD=)SF0$yLC%pBcNu5pPU_h zS|QCnuaHocPZtWEe(OFx5euV=*nw{f2NU{x7|zuj5Jzg@So4X2ingHbftOwMMD9yt zOdJ?QsJR-|@S5#YRZ#Ej@0y$x(pW7DQcf1G%>AMQDqX@NI?R{EV=VqRzwlJ8kl$np zm&0hUTXl(n_Gw6_NKG)d9o)HJ*>g5Pi#30hPu;EGVRN%$SkhzrUGvoPS)XAJNLC_;Ehv?xJR!fKi+WzQ?}AIO`M<5{_)G;1sMTiSvYrRz z+~X}adP1?=s)bjY-iw`J{m@rTi|E03!kd%j?33NbBZ8YVm5#Y}TQ`aMqD;@@)N^88)ayQZz$?@e`%EDu*+C8`40*d!h6j;1{ z4c0RVYM%VrM01F$PW(8&G8sXa&og$(?`Nq2(r;*iclukC&)ZV=Z%@8)ZVs*@BI1UN z?W(yWTnmOa2RaYAp3Ifp?@DeCcRLF?MP$t@g>`E}+Pzt`xN?Y|ARN4xU)CQC5Rb*K z6TRoMz|}AHC{}N!_IFO`x>8zg#q!gHAm|R}bnR|6747k;*brLHJtU4rq4I zd!{`XyIp%!r$iOB){C8j&fnv{)?oix4d8#*@M|$;D+Xi+ZhVmwWKl4ZJzqqnE5Ra< z2SI$#P?5xZER_d`^;8SSBpZ!o1AB6D2@#zX(|f0JtJy7%J>;gVve|-{?mSV*^!Aaf zJDi5I^?G5U=dWc8muU3H@D09EGo;%~#sJ03vuG4k0yn2bAN-CQeKeLU+UXTQVcQ1c zsA4rh7s%fo=i)ip^r+6+s)#y2?}n<2&@4=~bxGmct7?lV=&NyDs@O?mQYj<20mo3Z zU?bB!-u{Q_M*9Yz+vPLF(cG`2q2&<7)Z1KD>#;2c_G%j7_mFZXvwmzfHQDmHnH7@$ zI4WJNx^X>ye}-FSIz*VYv4@nEv-*+3CjDnWtLVIO@`BQd|0hw6BX7xC1s*&78v?`H z0+#NmMofS4U2lMHXZOnMc;BP<({{~u`cC|>eKG*au#qUHf6N+#{6YKpSwRfuvtLdT zEWvDIygvq<>2`yD4rw?#t_~*Y28?oeB_9+Bzi-Ugb1jN)KB$FwhLlg+9JnT5rSA)Z zw8t~|>hiV5s75srN30tzp#oqD7E2)?$HeOrlJIx~jSVeh`+mEa-&l^L*;O>bSR5qFqe?rqzzXW=O#KOA1eGzLf*@m?Hn( zk|=-SsF+emdG5F<=ocwJ`*czLRoQ$Lu&WV0|5G#Z`#e9XQhY>M+HaIi3e4C6IN&Qq z9i7R_DMSW*H%4u)zTDjg+wv>`4W`ZUT&TlMeDVfnvMwJYv&K z>&DVD>+%}2&;SEbbMZA7=0cSzsI8!pByXr!_p{qmWL<}H&dgk6Ccb0W zJFTP87W8`_3J^o&Kw$FVp+61RXtyT~nN&<2Bz()~+7X!oE?n9UCWcw23pig82GFuk zD>Y@or6iVgS882-C)w+lu%1ZClps6ren_%Oll8MCA6-N#k-2YqofJ{#kBtqm_TR0Xtl?YD?1AZv$Eg_jYf?G|JZ72&O>2U723I? zWGpXCWcjJ0TBwrnwbNi7YW0F7x%mF&$si;m3iVI&KPA#NZxp2mi%KHCh;f1-5JFGC zg~9JPK@m>!dth$`Q+ctYcD;H9WLAZSdNn<<*C|Nj$w~*NV~b@x{p-L=z79ppCgp-A z*@9b!mc@o#bP7g#TH)UIwK4XCiFjhf=jcQV4Qnk{g@*YRx!=@%#!Dr%$(|C2?lGF! zSe9Ceg|g^P$M|8Ctsd7 zFD75hJy*Hg>_n5!+8-4K;h_3!!7O1?TGJGf|Hs66*NZr zSYZWKA;#r}MNmr(?;{XG-s5Zvl^_TR0#yS69u)K*AaV~^jW(+;QcB+|c}ogb#j&zt zQarVEF~#<&S+Qb;tX-B4L!lM~Dg^@U>A~-_X@#az2qi+CPzZS3^gt}T=a#6=EMI9%N4D~ir!?4AHVn!e z)6(-}^!Rn#V`pV7O?w@#K1bqHlD6hOK6dWhDfjTU)%_1S=COD<+3W@5re^ zl^Pz@`H3$uXeH;60t6@)MJ5MJnl;{fuFA8}6_g(=juMN=!!*{QuW4>7QbrJ71?f7C zj{Kw_te%RNwWq*2FCb3=PpeD;=SQMBQaZCMpb03S4Hc>m7Fi2uRYr?2fpzioWeUcC z^Qm*<6%-dvW*+`B6|G+>C{iYqu)+`^la-#&gM~monnuU#BgY4pp=s4o%c=I{hgeJouLBdPOgxi~Ke zgUaKe2XlQ?ahCF|a`>~VeAc`wLN^#4bs1Vtoq>5BDp=*T(`%sC8(rto{8&~UR9*eo znuntE*N^G6Jb^q5%JIuwfPtkBx|~Ddpk+~VP)S?{$7xyKAqQExE4IvfMcZ#JTJzJ`#~8RD3HItE>;dD=s11UwAQ0e(?^{a zc&+1))$w?jUmo?e*1>unjCWwSV0s-&+vjw?<9#epb`Bv3D^C2TA_uOcway$@fLS3F zMQC&$eGKHI<5`K*_gn!_Cbk4?**c#kxk$N1Xo)e`lS8n4RvvtAEno9d`TRVrG&)`L zP6%WbX+=3Oul2f!3{;HH%QUKP0ezqdy`NEF z+>x#W%hzQEi?_yUc`VIRL|$F4K62h*=hdn2={j05;kYAV#WWn8^ksREhd;(?-4N1z zSkc2BcR$@P7|+0<={Vk`wNmhXXw4INZPFIcSIC3YSWc(Y1~+{j)TFiC-G( zr8_&UQp13-LNj*kSf$kPS4p)Hr7K3L1pz^zJQ3h~(@kGEW~Judd+${r20YZgX^pp@ zvpo7*AFXlLbER~c;}S;~tn^G57><-~r7uLTP#$Y~u=7Cvf!CZTSe%uYwOk!< z&0{^+>AaKmhK)H`u`&g_-h!I8ZNj+%jV_C7{p;J96AA%7N zSF|sLdgSBrf)xineyA)Q=g+Itef0ThEg#2m;A?&ECoec0DUAsam6y}Pc-->1;r4h7 z=z{SyLur zKmbWZK~%ZQi&Hz*Ld}{HWT9pJgVPs_?(EJ^!Qb1{2P zD)#Slqf3_cJ7gVVU%)=HMR%rMrK@boGDh~qId(t{svV`R| zPy`!x`76n4TJ{ghe4g*hgPZ2Ni108P^mr1dha&aeBvWC@cezNhFc1;J>E^&{m06$j zz^BhO(tNGs^}uMQG1CJFYc9l%#q$;+zJ4+q)vrOO`=W{s^VfV?D4!=A^Z#9l2F+Td zUX3V{mmS8n6y{Nt@1G!w$iPYg5%WJQ0Hy+CDn|KvIq(>E!X}i`3DJN7T;~gn*Clg0 zr!ij+%v*-%TI7OppVt=HSE*Kw`$B%6=xr*@iV`I$|jHi{) zq0$v@K@<$Dyj)JO{M0B#+NFq$bP-BRIU6_TVg)HQy?Zr8WMqh1g0j*To-?*Ah&~pk z6g|`FW?PtPf%!BIrv{#LoTj&)8`NKbMvY=nyLJSA`E>_s)yhS3ayYH}Fg^WSS#eeh z8W;1bY`=%}det40kNmH1A{}40VkNgAAP5{S0{R}*lcv5$1Nmt>YdpvC0HP;HD-Fl9 z!*kFyVxm4SNckPkP-N^_he@A(j&zq-DKDX+cKkc{Yg~Hst8l>qPv%ZapM>pc`3Rwj z*i8Qlm)|xLd29jk^V72MQOlxbRM&o4=}=@0;<^XsCDBySeAt3vC#7b>1Gix5+U;sn zIE|S}Y5uCQjrcnpw7l*~$D;!VVC}YaHJK}=jK~-h6PLiT(8Np0Pck`ZMBlk?CDnZ@ z$nAkgYY;T}{p(k8>nmT8V&hPC=aN-D{e1ac%gt%ly75HdCF_=xp3GPN{WGS{r43RY ztQ?Wh-aL%D*9V*=dg=VdeI-iaVyyo~|ahjf6%tMOxij}$O)S)IB7ebJm>m$ntHGR;3>MJT< zA5A*?NI(I-d9KrGvrf7n*-uBulish+IgXc(m><)qBk{4uo9WiP<~hq4L9%uvnOSz` z&D~XnqFu!w6(rJ6UfM%YrAC9Sk5;DYMzTi8xga1298&~JdJk$keIN7W9YWStA3bH~ zk=2Br`w`+_x?oz4&v)g+MgKG_7NpUr3mVf99!3@x9=(Y+H!TGpj{6wdWLDDGM#5xrvWyb>c~d z2VmZr)=9j5;AJ)*RGq|^>q5zjR@H;^(kN`r%jZ0~yYgJ1`2dM+x*?3pGPqD{u=1ki zsXU_hk~O7emo9KRsHUu>I6~;$PMG8=%*Z67aN#91mXAypNT;L$|Lzbh7GN{_rstoPcK-4()D-eyAuv%!)0%GfC^NS;Uh@l#v(jrCJ_gbqF3!s1P*@QJs!y;} za8@>ukf5y{>=wdGe#!rt7q=DXG0!d66wh%wp1Q2%+ANFXbY4xT`RRCd^zp-qiAp}* z^DHyVo}bSaQj>2`TvYk~jAr(u{ypOIQ?Q0Q>e3{mARq`-C<6L^<)ise(;rR5-rRkd zFn$uEYuCk^c{4C?)lQsq+4Z>d#(ro}I~MEy{TnYnH3(S=4dKhq!YSuoid(Piho%i{ z;lBlQ@%_w|IP3HlnEA_($h6ar;~6_pyKW7u~kfEd{J-@kaDUT#>uBL@u|*T=l&d(lNH4CbI&fDyGsC@BE{Et-RO#(sbunKm@6 zAA=<+Zrn`x*+|iFr>EeXuRg})SxXRRSDxD(Q@;BWH5xeZ%8Sn+B{Kw%KJp;EdpBY2!FuR&YB)A7{0Eb#FF>OP@%Vo7 zS4i{4;kLUU#HBq*`LcU3f5w-1V)#TftXB(}xxgjY^hf`zF2(j`f8*0ha}nOghavwr z7xsjc@Zi0-VB?Bas2`Sykj5d%$=HW!lRw9oKmS9DkQ<$QUWFU`U4q1fXzX6Q2$Q~@ ziKIsLF@5Sc*p%kOb+_JwYc9V4;q33@pN)9JWd$<Tda~t;4R^D{#dn=O7_A3?0wx zgQu^VfQ767L&p~NsV&?zQxUjo!Yag9_do9Je9jZM#@#xJTjMn?A2|f3nXx86eID#+ zR(egt$0B(NlVWicq2%)y$MpJN(-y6_$%LDRmvs5a%7jUy zaxh0eu>@vr`nOpkxsqy&3ifi-WilVm&Gk`o@i}QQT#mL(8q~h5kZ>^NC_LvgQ&}vWY6W1pz@I7y|lU)kjTZ4kT)L zqX{b?EB^cfDGBG}fm`lDUu0n9t^dQvwd&%Dn=XbeBn($QGz86CG=<%jg-^yjjIrO= z!YdD64OjL-jCtX4EWY_Z^y_gpqQm0hJx~K1e%OU`F6xa)8Y>#sOCZH)20s5iAGciD z1MN@ij4$V}N0)OhLc>Tqk{Z^*mZb~u*{p2bdhI1h-M$oe_w0#YFMW*r&hLm-fBcAx z&%OzNESri`8irxpl0PtRM>Iwayd4+z>4%So^})N}H^Z>|uEd^|f8f5m2jSk~uj9;9 znqu9;>A3Tu$MN;#FVQGu4<4gQy43@3W9U7dk+p9NcF$jB8AuBUVJ9uu>{?B{}s&6I0?gF9Yyub#D~Klz=WUb;L)qP;q|8m zqCuYrG5n!Z$vU$cGv+QvUPu?5*0~#&{kRsr`dmgMxE*!qX?4NuzwmqJ>F7_(LU~yk zc=5#{c;xxf82Icf*uCNp+;H_E{J!i5oYo`?q0x0Pa6-^MMUZpHN1pGCYq8?Qfk9>(N+ zh!-Ea0lvI!Oc^#5od*uZtplG$&aM^cd&3=Q-h3|3Zr?~*!yIJQ;mMB`98!gmp0gkE zHJc%dl*L@K;5mE_G)$_ERq1)OB<9DGI$Yo+AFMB0`(?0m9jA|hJaqh#jz`LOr1T^4 zFL8R|oPF3Vf$~_d`P@GhD@%!K4I#Wq3FW{_gF@d@mKn#&4>eE`g@+UkqEl@}`9ee4 zf}&f_w6Jh_&QRKooJd{D2gX!ROc_jB3UvaVMhX%2YjWu#%VJ@ftPHUNcC15CrKUKj z$6l~w4TO{@2vlVR%)zhlKGj2&Ig0U97!6X+rg-?i`*1;*rt~E4!Na4*;F(Y7*_g&FbUx=6j2!w58rFz}-5G{m%jaU!)LH0s z+UcaU(9(|GhqW|;H;yA;5AyeD(XI`mBAjT~u`4<>h$7pH7h9JA4HKfsN@HN{>VJ{n z<1yTR{iUc8?nF|(n=yFycldMpDzr`NhWs30%#)9y?}c69Fg(bek(ay#C=?K;$nrPoRG8)HVZkKopWtACOp zM9A~UjTNMbHM`>_lA|wT$%)t9v?h{2oTQeLvPq$s_~|dWeb_rxZYFYQ0jO>F-gxJy ze{o0eR>*K=qZ!Q%kumY8-{35?>vTFo=*_9z<}DEE*oUs&&Oj9H)d8OiK6@l;M$#;z zn#hOV7vGP8_uPV7u~Eow5{EvMr{iE6S$vu%pxtR_W9RlQ*uH%$Jnmea(yKF4(o&I7 zuLD}QZHm3#WSnv4Ik1t^m7kN2@WlGcIXf?BA3h)dJtmF%3}mBVEvw#V&EfpO#*mg_C3--)gJvPnU1PJV1;=~2oQr&Fag0~VVy2V%}Rn02!da^z74R`VnX0)jvl zMZh{}aTN-#E#ree|>1oa6CT!d@njP@ z53F9|m7cO49Xqu^2+hjbj0?%jj*`45NOndhYSoFt!asi_|Dkah_x?D{U%ef@&Tfi` z=(@P)k^jTDW1qv-eJ{uPeXhbczs(~*8ZRk=c_hDAmCe|Alu3i?PS|i;aNTU=FwA~G zIh;TYU2lz*J{D4x?JTIJhFR)@_!vyHDqFFVTM!Tg$^!wu zH~Ajb_p6ns8L#vbzM<716-t(cP*M;;Pv+@li8vUSKoejpCcO44hJW!3uDkdw#6*Q+ z&Qu>>{UKZ3`#dnRV#AYj7!80t;oE3H<0Tq?6Do`>E#%9pWMGF@4wOohIK2)`31L*8 zjh_B_U{ibA(BK*xT^oD1?xqRdj!1Gq=C^oK$x;y)!TX``h)-o&#vUGwy*?_BtPNx+ z!bu&@#o#Aypyd!!p6E%HW?ypLwPEhY5M-sKz)O6is9b~JF<0`agHj}1qy*T=p*D;r zg7BCa?B1QG6pAo*l7c;gMn3d>{k^Z@nL zC*|Lb_2lo;@8-wRdg*Uu)^{SGc!Y$7BAjNP5F2R^RCo0R$sn*iT*ZR=(HkS=FzlqZ zN!{`v?tl6n{IXyv&T5;4P|6#!c?L3QwqRr#SAH@NCEV@g^Wq_gV1tz*!p9ySg~$U3 z$PXt+y$KaYij#|+hto1?OAe~5!jqrM3(6(fRGSdC#Pf_xZ@}3!d2!&kzMNE#sIVHy zJ1_^?WDtlaE1bubi5*)~(6ChmDO#k&np2+QsWSS1xC|Yv=Q^%5$3x{0j1RmnOBq&P z*7HD~!P0`o1?E%D=)YI2^hM7L`w64TkW#alM?R;2S9Uu6&>L@B;i0&EvLL2rv0_aR z9i*t+L&Bw zc;=9D?4TPiH`B~tyLKY=V`5DA7h7cZ?V`5|W9w0Wi$hY4Xj2ZemnsE}<3eLACE^IIfkmIa~d7M{xJnh zmaM^+&6|+2FI_#!<>b@;B{ac?gvZb$-#=KobQ!j8-GZGd`)Do>QE&RVNjXVAxgEBA zITYW{oR4g>rYxHKBLVYp`z8hcv zJO}&8nzC>A7EJl#b1Yqd0L_wHWA^9IMhO>l-sqG8j{_;l!Fn7eQ}c5T~;pT3!l?`Nf|&HAEhH^z16C1Kbb?_wFf5}rxE zFw5u9#+Ot7LB_6)c=!GHv2ND^)M=1}qy~xXP(Wk74bhPinDNs@EM2n^TQ+XQ-ZWA& zNbhp735WtaV&>+WmMs-QKQ0$pT?jV^y_wV;x!ApHCsKB9!^~;lV#3HNAbm?Ikr6R< z$){%yHg4WbdzWlh7B_mtR!fGVku}k`UkAMK!N*v)eLqrnt;c5*Uq`D;u0g%{aC!sH z12_4_m_5SmBdioDzXyusH_qH@lR~zZ(VK9jNHwPAgv89rnDW!_NI#f~C4WrAurCi# zJCSvZmIc-Fg1-{n=hVNygu*_kYgzOdEM4;`N@>a(7#Dc0`2`*W>GZXx)7OE=V6spR z1IdbAYZ`rI0xK_FZm_sw%q$VqJ*chx|0D)046K~9vdapL{TgQ% zp7F^*-1b0!j2!U@uF?7FVFj*(_~!jbaDLC$82t3@829meu!YhxGj(y6qd-oQ!9>jb zwNyu)24WPLel*vIwA5Ic=|-@|9q)4n&adbO73E0jK{J&xk_RPzaxST5%cN)1Ul|Jy z$j;rToLQPB91lQ3tbm^RJuS5g(B(ua0>UbEee8u3h^u?#nOHxp@O}Ud%v^CXI`1Cg*fUB8l||QXCq$ zJp(ho{TOeIeHFic*AR}pRAfdq!}Dj8L+weO@Ws2s@#=Gr;)}ZV(V}%5+;nv>vf_|d zK^Sls_K?MejqonA;$Hl7egMo7xfw@k^@-d;&IV2i4KJpSmKY9<3 zKRS@!R;q<9Ysk6w!Qq5WEONFlz`%jC(5!9@cJ4Wd_a}`(a&7WCifM`0p1%!ao*snM z&{}wH*bv0T)JB}g{LBn1R}GR(Jk9pE(_31}G!u9b->?Oq8~ZXwKJy^HNosJ@Y!ISyZX%OIq`_c{hX{0QPh zG7;VQbUbrsKZNmPH%)X6PpVIKV<^l?CbnS%vd9$my)gR@FVE#8vQ`s3Gx7@z8}Swv z&HNHOcI~Ei8iRJpiL?~vQC2nPRpkCMSt~ZW9BY3pLQ4t3Xi6H#ysWhPJn(2O-+Hd; zbX*`mjt``LSE6y9tjy@F4EH<016Cyes=kQN>>3qOV$|4U3l?G__8vPnEPxfTq9|1ar1$IG|M#2S@4b0@ z``#5WG5_4ay_xy?d^GqiV@nbmhj6<;H$1|y*f-^nkZZIPLf{_30(}!ZO z1GmG02ON&I(_hDkyT8G|-kOIF%}VjW4SV6s6}vf;-GGL=HR%^Elk!&npnRm^Kf%^l z;B2s56og8S_`mt)8?+RO8*admAwv?M zFwsC+%OM>)bl{mKu2tdeL1T90KjXZiSK+vGuSWlk*dcQpMqw+y9?iWU+UudSOja&i ziWRF%QP{eT+X!0DPG3t9IuAzc?b>)41T{iDSB1RGdD1L5UGQ5k&0Nds4v8b38YwN6 ztXRIB^DbFvk)I#x(it=CM2Xe8Gb@)b!P4cc(7d1(idq#oUAlbQKcYP*?rj_@bvCeH z4r~ZEL9XE*k`*gBJJy6r<{9JR&eUSiJQ4uOSo;`RJxMa$(_R^IV^BFVLB5>%WFEDXUXD4JiIRp zZCbbHG5}{z=r8SY(lsq-o4jK68Z>Fv+|95#UlFH$H!4?EMJX08Tu8rXbGf0NTLROk z4YWk1IH8xNsNn34UR@V&e30HOersr@+h>#8j3>pmZbg@~e^xn~HE)awZ`^^` zf9iy*Pv+Z=R}aPpd*8-43;S^iX*KQ~egX!c|2TSNahdb#yK&>qBf--NP*t`X*POC5 zemCMR6fGKy$GM&FKHi;0tF;o#r%tv;SYsR5756b)8?e&Nz_zkdCAFYqCx zp21qFSa#P5#A5zEKx^b}6c_eY?nzHGxlNk#O$(Zm#0MijJqh~wU)5SOiVfqWk3UB1 zu6_6>N1+>`nq9trD^rz@W9Pbgw?13CGcQbxPg|R`=};ppUzBTuCM#4P-{e@Qz?v7- zJ(J8Q+ci8-glqYY_0;ea zw=8Olg68#mXi%B;S1l0a>Xi`eD1Z2TqI=peEG$AEmvXeh)H)_@z|C)2gn~j1AaqC@ z&ne-6MshWktu`j9Lwjs^1JeGMty)Vs^OU5{k^4NHJQ~SOQyJ@s;+g_!1 z9c>QltYp~CXf9t_HQDN1q_bUzj-ecO`aE>bg1!NzK}yS_HqI{T56Tj>H7#1UM9acL z2h#^eN$dtmd`BKd92}b7Y|7M7PP}?sL?;gmXLe|>lcNpoY0dM5bE9c_A`$l2*Qk`5H5IB6dOSFH= z1>MAch}x1VUn9ScyrV49rAjr{slCQpcuzdh5KBgp!}k`6&a7U;bUO@F(1L^*LzK zstCs)e*$*jrZ4hZ^u@hTy^1w-o%zcAXtI3$f<^O2mo84B&rn_~$M}Ceg66dGm?Mus z%lr;_?!7Oa=oC5Wl*Pcs7sspFiL9JfxMAc-^z7Kuoj0PV>V)4yS;0PvU=~4gf;uUg z$&NflK54|`WQDpDJd!Cq8&M6I)R6je%zWX?I6%=`LoP(+Ft3oXhgVP6SW74xSZSs~ z@>A?$$uz)FXP)5myCz&)%mO8(>&fk?iu)&qx z<1;^5dytYRoi-!uG-1~k@_u)5c*R_~DOF5&hkd{~j1Wb_g^VQL$?6sCPT8jpQ8Ey&3FtX!P<#smx=(8s|vWgXbd zWA(YuK^e&ouCW@az(4vrvi3nZJUQiuXb_M2M^pdq$ay{;OJ30FY4{q{3D@{XZWNiU za)^HQ;@LQ{T^F1)5xmiV#EUOJk1=CE!t0|i!!1KcU?Df4D!A7dBUA3qH)-mL>a1_O zvss07Az9u^=jZh0)j*17V$rnEG4S^%;K5fv!rSkT<%Yh?ap4m)6jqNc9Fa%J@y!gP4~V=zfjoW3Ns?kU+)8@*1l>WceQE~XC-Xjf?iTaSl)WnmbgKU;}_DB6<)h#JLHRa)rKb0UgXS#f73e1xy z8Yh!+3XL&)w8BSe3>Jkuc8}M@rHf@Ru=g)4pzore%jdoQ>n`9J^1= z;J0YknSBG>Rm%9?foGy@=6kqo=zbV_*J#}Oz$kD_7-v|UqNq8SSgEu{8*T^}Kk3Ea zs$)040auPL{r1K+=bwy|PZ)@+ZybTV9v5Q!4$Yig@wq?FB#BS92Qa1L!#epgA~Nlu zMs#&k6c}7tvIMtWdkyX%^$(nS@UCtj&bcE-V&O_ni}Kj*NfWRO*Rk95+MEM`T>NqP zotU#|C2{OXKmQO{UvVoYeDF3dIPGYhdiFKA_Rsgxk`@?t_8+n5Zhvqi(v?ep#<*|i zIY*7Y^)^?eTZRqA0lV#kw}$7ddR6)VfF$|=LHSzkCWcRfxB#nt+w3;y|)^Sw?6yc@ygyg z-+zKrf4>u#dJ1vSQODx84?f}90jUk6jMD`bbn0hs;-G;WBe!KQ9C7rCczZGr6AP^@ zU(MHQZyAQduA5_%wt2Yly8EzbRp7Iqr%uAzM;?gH`)9!vQhnJFKC!o$vZDjGv&$uc}~ z&voe9eiQWS+yTS>_Ar)kO;>(f{NrR?efcnq{>S|o+>d(sX3brXynxT5<2Gd4VdYZ@#kSz z;luGCV8nStaQNw0U^(}U{5<7j95s07>h*-PR3nEC!-t>SAdds4_n*gAe|y&HUpjpf zuDSe5{Nuj6v1@PnITs_Je3N?`bluqqu>u5*lb(tLo4?_GfX#;DsC~C{O8|TR;Y930 z)Wk2p3kXzc>JX?QmO60{b6}(Wae4w%T};3uTm}#9O_do36T9&^QK>8u=>e@z@29H~ zn{3MT>HKt*$8d@*EgC=Xr+t2(-iKQ{!RM9HV1De=N7F_pxJ!H2ErAQmtGv;PESXO& zk0YwbkLgRujj_e*Rf5LzYZMnX!Fa3;mbW+^z3@?*IGD*z$nmaA0_G*6GNL*>QkQgi z&+h_e;xU|JOB;{nEp`gLA0MuC@aGXQskS3HrB^ZYyq7CE0NL%}OYqhkKjMN5@5F0k zR^o_#_jgO3C6(FO_oO?p!{&W>?^9-Wa~yN--RRnqd*1Rl#)TK(iIY$LGp@e&DZKp1 zSnRNAAFNqbg|7Yg$8r1b$3asS_CD$F*irmzs4zPpN1uHMx)HjVmMxY`eR&v z+3ozq$xS%*u*2P3hLt?~CKv<-b_S&&KZBLik1ZY0t^056peAr=>V>IMRbVr3i!!`3 z{2HA0kFhxNqFeCE2d`uF@H24>8+nB+J~3YS;uuVP|1C_L`VF$)I~P|z_=>}otXzse zUw3b&PkyDrDz@cO>k9iE zd^9e->QCs$@0!e?JqZ{6^GkOw#LAz)M)ys3M7IOa#k=pli(yA@iCqWu#22%29i3Hau-2XOw$hvHx3zC*`eeQ?xqz0sL|JMr|haQq=W3ZL%> zzJBgdOrJ-;F+4wVIF9<`OI$K}1Mj@^IzBn?L|pW@ryQ=lcqK;gn?TQf_6tTlHW_1{ z8HsaFH~{xNhA4%}l0-2UvBxbNeM81wW< zoO|+t_;}hfwCTPvmOpYA9^*l0L8!u;Pv4GLayz3<^DK;i=z0tveJ0ws(9=nBc2Be| zc}9aakrl6AiEFOE7K4V|h)!GWffEkd%f0HmRp0Hg(Lv``uP5Aj`oUkW!!L{ZWwtOZ zUpfodz7-5|$~eV)%?*FSyVHts%QIiGy!YeGBL`#5gda$)rIO$&-wwc%h!u-|K#yJe zI(wYRtbAJ;cqyKlwkHxvP!GF1^noYK6*6FixDQ`-WQ9p!QP7@0d4 z9SO>?^oLw@$nT6nyX}mku%)l6%E6W!1T!g{ZLl?KJaYqfeSRSx zVs7%d7lEzL!4_SM_|d-kXxAe+dVKb@SzL0NjD`3EZvN95D3Ga}^ul@ld-9Ei@G27I zDk+Cnvg(ufpF@kouEXVL{lP5__3qss1NCMDD-Bjm$7we_g;CGG%kSfhuz{5u1 z$ZN*p3J$_%a&lwZ2}*c}7-r%=2_fAnfUpJxmVOmu=(*#+Ft$HUcJs6@ve zr{b33L;1lv9(CWl2;I-P0!uhEhR!X}f6(5TJL?Dh@WWK@O<02iXv*A0bCKPA6Kpqd z56rLJ6a#nIE)cc07|r{2;hP+spTtu9>HItJ?1$e4A0Q;;?-O1|j~x!cUoPT|7dH~O zDe8-RZoL8>qCYnhXD$unj?Y#0#XXhq`RXF*gYw?%Aj=*Lc zvMgJ7#ZEW_^Ja2T#A|Tbv-e>8LvO{|N9~DLt7qWYD@NnBiL?2Z2JrK{9OzyWls}7u zk;xytgqLY&o1zwIlHVGKA9FZ23~M0S6yJ?~0=?TccNUC)>2Cbe|2$lIc6>c4;u}MH zd&WWzU(4ORO_101?b2y8-1O&*uz}V@2W*PFc{KfNUwn`KcIb(>o*0c8tNA7o=fQHC zcgL|ua(R$%0O`$u#BI#`jN27h{`lof)0-}iYY^gK#-9o2j8dU8gpB+d_zV@t9|tq~ z+QjwYuMdye60OAsPZwvRua_Me+GdkyaXOm}9!D1kGy0n1OuU(J{|j|5TK;I>_pRad zJfbhvbANeapdgr4fpV=cg~0}+%4%StkfgQ#DbezTG|Jmeue9MUmJ|i!4`?gA{w~QA zP--mTmiZY6)Ecx27nK>%udmD;ac}Vm&z%w)A{7( z2e94mPQ_kFoPopk+!{-`BfB|I5mnG@n;ZA;ik8io=WCQYSE0C=3x|Z4@JP!l(rGV; zJXFjJtWd(&Dc5ixhE6_26+2wLPDwl$v>bl*!kOs5#}?rDls0iE3rDRzw%iC`Eu~xq zUL%cIEi}q&fxha98I~`c&o!f&xaYQ;u!=jtd2lNBJN$HP$b+nwOqqqAf7}!KGF2Cr zU+FF5T4TT|ZhsBF3QK2yfg$^x;|4UUXcZ+qZk}Js&1T12ymTpAljDu&pTI}2a?>GS z}C(EIgMBO&pH? z<=Zjk{3@}l!txtMu3b%kxKZme&M|>s{*~3iHO_C&!`2x1)}R}hWb<`ey(%lk++>fU zUcKDg7mi52;XnOXP#w#moohMrr@?LptxLa6C^@)+)>h_W)85_O+Z>|boP#moEPT!x ztqzOc2RfdF9X9JsL1k#WGnd_dDHa(6%g@0_+jecSdiqB=^SXPn|B>8NMSb16^yMqf z8s-LJ`GT3)DnmVKl(T|%2KI)IvQ)0RFyk?U4Hy~)K5!>pSOn;*XVf)M3aJ}QN|ag z_1Kb07Sr#rYsC|Cn{JntyAlpDf&gZ8QM)WJ>P&j$_zfoB6gf1)%2Q2dSXRM(e#uZU zSTqihfuBsK>9o9~N%P~=TG~jB-*gIYX+IV`YPaEqb0NG2f-9J8m*J03Z)ra^I>jbK z>8M`86ntK%>wl!~1*K2^#`%g~a|Ahjqp+Yw6^|7tK~tWIWcxtFOB4!8-Z-i70G$q| z!Hx@@2ymA^%!@vOGm;oshzezdIm%EUm(9I6%a*NnwyO*_z_M~Bcq!NJw*+h*60w6C zUtY9Q`?$EYd{SP{DJ@P(nej^&e23XRHsDzq%Wwq;G^3s$kCPAD!Lfhdy$XAd3in6Q zi`sOnC)VI;I*?G=xCtGk59Ng#>2ce;qCR$UC0}{eYnV1Tp_BAY8HL8JI$`p}>8^Xs zaZ5dGG5@FODBM02MqPe|(vII^euu{Ka*QIBO((m&|MTv*P4L6_+;L8zlbhR& zl|VABR`69weGHK2TBJ}=#ZWES?b292Do8&jeVq(r8b{)3wRpe;w zN2S<{DiM3yn%fW-W6NTFLh^BENrZ^M0k9B$e?1&Cl=I!2c8HlE1mZX zkICn;*R#KL1DXj_7h(IJ8k}%&_w|EV#;HsN4UROZbe@pk1kP>6n9`aRIGR;FyMhOA zyL2f#bOj#aL4T5zqsZ_QIFF<(1+mc1H{4sL*$ENxbNBh@$B;- zxid^Sj6n&?V+o~fzkJOWStS3cbJ*nh2rJTIvEDA5Fdu7=%>xb8~2@j62 zD){}xcagu%ehz)`Sws0%?9`B7v@vGC_pRG^9`<;qzVVS?l-zUH6`|);aeTd%yNh3i4l{fzU?rGXxG< zuk5T3+$wKOH3lrjxG4xbjZA$4FH=fp3F5D5=E9zLrXpfbt$}NZ<1+6%H0%_e>}N#f?5S54fB0yp!@Emo+xn!1`3)6B2zVZf(4m^w^zP^o4~r zidB_k_}7PA9+NmCK=5nUHGcXLv3z}pi8jIOCaDOd$F?=OZ5r^~?Dq~og*YrkPV7~G z*A4(|gr|QNUyutNn(?=47j%syY-IkMjsc?6wQ=`bDCJ=UU5(OU~Dr?U2J z?{?44=b{n3vfpf-EW}t95;W{MqE=&GnxmG7|}D`$K+hqnR^y1I{1LbY6#z zg`Z~|@wGEQ7ggpiSO4z0)^Xjm&nM;~kr~Cighvl*Q>h={{MZ>1ZN(~2edI0o@46R8 ztB!nIO+B0xr?_wavk^8&JDdqge2&#=L@PzRhrxfT1w3k0`5PASoEx!jR#3QjdZX>-wsgeXpxK8+!G!Ki zuxq^Nnjd!0u^%n#A*7N-@GdO)_i^5H(44R|zXLeCWLimwAD)=D^`ots@Ev({!6&h> zy*8YOhnx2srDX!oRq$7xmE%@@4dMcWxgOAyQS+r**gVdaaxjH!#FCnd=;qliS(MiV z)wB1aU56LQU-rPguJ(=NrNd~J*tH&_Y;eylHf^jo&Efap_% zHG*JkqTIW~lx1KF{{2^(R|RgJl>W!#MCilY^?9bNZXSO+`hbH3Jd&OJLpq_(Q`L+8 zTFk2z|7Yg@*7%|Z!GG)Zaci9h)#g15+WCF16DZF|uYJP9Dr8(8JfP3+4$A~RcI9X%waEqBVtwEQYN%UDXm|S4lr}1LHoR%#GJAjV>(73tDZ+oyt z%~Z}UeDgW>d3?iP3X?-Wq0wbGhKqSW&V#yhxKAR@A4!5mFcXlK8V0y}MsDWC({@Q8 z&W+dg{>cg~yd;tclvYJ7ntq?apfvpzXqty{=&g0L5THX($U@^`Y#%(PNq5z>wmMye z{PF(T;z1YW7MNdW<*b|u-g;lK;HZ42;FLi>`9RuLHy^|#)sHd$NAgQ=g2@%Ms{JTZ zufEtyW6t$t*F%asY3l7ztD7w(@GH)~(Hplg^QFG&Bv1t_jRc-%=RG$e@JbOz6KQqK z6q)x}s!)^6fbt00(jkz6SDa5kd~sIkMKQo0H3BsNWqB_jv$F+8pTW&LeQ+h;QC}24 z5OR3Rcg@`?3zs~)M`Lv^BK1_y-tidcF4O-LcXnsJ z1r|4SmErzp&v73Xz0QGMv`Lm1GMapADylD%lAihC03cdBw;wiTG<_M|#FEHHV$J1%1+^VkVN|(9 zI*amNK|Svd1&tz!iE_30^MZ96csP2JkI7G7xQrG}jk%|_E=_LBhh=89%=_evg_*ec z44Jq9WoNN1@o>v(ZojndE;7#pxjNAOgZ94g7w6}pGoreW>fLi|d0v^b&(UsSO0Aq`(osyo|l1<1}SmXICw#c}IfCO3D*2940xGEDn#!C+$=Qw*

k5sD*y2iVi#J3{p%ePEuUcR1rQ``fG^e?uOTOBE-C_`Xkh+1ki9#cDQ&x^0sY! z<)h_-m$BogGF@5LK$3aw5lwC_Q1y=l4|g78 zkG$Ys|M}>;)Z}#yG*zgTq4|+f8CUYMxZlh*niWMP_ej@u>V-^T5zSO~I!IOH;5~{i_=RQ%rU~$UK?jx)?ra|Cm%)OSOMAJi zfwY@daKhkqSW5)BoK?k?b0f)ALRQ(5dI+QQ|JMTGhBTg|duwS8-T8V7ZpmJE0b5ZF zCA<)3d-AZ%-y%|=#NM4ps(1?!Bb`*j)m$j@2+bWC9OV{37qoasj8`58*+6MrRa4#i z-8WPV(k+*u8>b$TZ4{SsLT+2&uqA;6ECR?@UU5m>ke$mHXR?Y4IfyA|YJ=how(vV) zOvl!ZSHCZf;_*%=T8LT`$c3hjoyMn{Qbr``Si8eu(fAe^B@3hm4%#-oI$$`npr%q@ z`Av0!UI}r2Vh7x=vYzi&OO();V(a>tf0;2Gr8kIK*O4DYF$L%f+(`Hzl1;JC{l-TccaqH%H>s$wS1O{=t(9YjS@FA7Y|&S*~O^+C@XoVN={t`OlVhEGBJuyZyLH$b@!fsQ_yp1DI#>EZ_-^uddsrZ;=Q{p(9Bu!3L|IoUV+j=O%yr?MnJ^Xr@qujUK(FfXg9x67vVf}gLMId}y!6{Q9-5{FXCyb0Atc;eSlc6&`6NKmA6rhJY zxW2Z9MuiHTD>qh_NL?SeNIHCF<MD1&WM#rGk1<5t0e zIT`DXms+}-4eBzhS|#;;4gmqvJ8b8jY_2SS+flIEpTB_c-=N1pfr@7iT&TFGX3(DsB8C>3-M)-{W zAnsz6F)H>vX;S7J98#S7=6v!#P};(c^aIWK&0x7KgPYeX>y^IF{Z<82$=5a2&w5rKSxvKb91vUpd3N7I9bVKo3?$Nqp`}ts71MBb&XrxsMxR9Z;(1lAfq+zt zAQ`hopZL}0C0JikpmF^aSh%=0Bz_`;K51vWxYNkEUND5H1W0f+#81sYktQO65T=HX zm>2N>vYPmCntE+(wCJNAd3p;h;{L_8#qC-bH`hFaiZch%n(3yF*`LMr+fXe#1WE=N zvPb}mdFP&12G>YEr5ar?o=e**3G_>XpBpZgOnv^oY0@c_ann14CCs9TnYuWUa|4VrvE z>xIU%v?V7I@=!$%z2i;bp&Q=RjqQ|+MaK2_TLbqkUY}TOS}!=&o>Fy>-8#bm_g+LL zHx~!&j9mFrY3s%_vVIQ(8VZe!cshpZwPPD4)J0%f_WF~f{#-|GYiW)*Q? z*-{!v0H}fjduP&erAh@xo zNK*mG>kmJ#AE=pk;MT+z?buixM_ue^j27~mt(^NNU!8-let`qCP7Mzb$(q>LNGAov zRJF52woF(x_*7{zomy};*{F;0%gOsN>>v~RKO@o^(f!owkB%$5+}@H~Oxz8?e#BI% zWgV+P>=wV8iHo_eIdqfRhS*L_`PHPM1cx$^A$5J`&9I1=YRVAHoPNdK)6yj+Z8A~! z>+e;5RqNQWz<8s^=rwNda5AOE^RI1f^R<4`Et)5(C)snY&{`l9&BTa=;%m-1UTrErk2kLPDAnbueWZ-4J9&=ovcIHU|`fvr!>pV%eA3xKk@Jan+ky!&AN z>k88e0p)Wgig>}v`?MPl@jn_eE{a2DO96{ptR7DYyV{@Js>(#HHY_3`AB%h6zYy$| z`uMM42~)8o3s3TDS-dhe)d^xp!*ZQRzaONMv)UT2wlk?bPMU$JUL6J*WCFs3!gu1V zMXI`!S5VWQYfa*rj_2JIb@d!9;jUJWV&5{v4z{t((T2$a6W`_Fsy?`QmKnVTBTYxGmU*2f^rci_qpq@&1 z{iD1WrI76~-QCAvz=AM1wOt1N1ZL3g8*l`EFk1We8Mp&-i2S!#H3@>kI5i}M|N7N= zLc`dP*QqQpbjHJ39WXDpBJ=4g7j|K9y%}e)1f##ctfkj%=h_$~VGVraY6U4_9uKt^2=OLWzN>xT3;U`Y4f>%xB)k$eVJ3b~jv zKi5CCCZ!MKB0@LA-zBD>6zTqv6D*tvcj*dRYkLvMCiRLzfV0lsXt7F*O%$XMt<7ko zG2b2VmhMvvpnj<@Cpgg;lE+w359Rcbr+1X5!U6snsMe{|dQQEP)Fw){8J2v5$a_si z&N8gb#K4etW!X;?fkt~GJv*=r4$g0YYA`s$0%=oJAdKGc-Z{VVd+jx3@H*+r*J_-Q zv?zKzo|n*^go}{Z7pr;RQ_&h;AT$v`3x)<5KV3(mm;%11^XYmgvuZ(Z-yO|D$bel= z;2JvCE=B_I-06x9 z&~YLusN^fh0hp7FO@A z#d=J#ejuLRsjSw8PKy`*V`{EvE~fjj&Q>)#xHs@a(~WjH%E-7j^jK$&w8uVQMftdg z+trUdmpx(9c=ZX5+Yc>Q=>j)nbXr8`*(j{2wJ6}AVfQ-A>*b_t70xVAKNZc{3UzO9 z5lsnXA>guC*UWSUpMbFw`*pUB&57lS*rh5>E%>j)>Mkiw6$`rUGG0GGrgMSA$CNK1E4zyp zae4W2d*|Mh)c!6;!#-IifrW5;=_%|j^xL=Pdq#@Vklr?6iMDyxp1wRTiI*vxD>wNP%Zt5WjHX1^+9<%Tcwe4VACQ}y@O|XhCHk%*>DUZ=O+=QC zbs2VxWOLO8kh-Dc1f*!>l6jWTq-OtEN8faIY9mHz6+u z-cUgRi#davtxAZmdqB&iaD=*_AYA?+RC`m{ynx^Y^VY+jBn}1%iA>fzuMdcu7mF zNi?D5!Cm)35^vP6od^F5V3CO`kT*!{#7J15u;{u5Sw%By)Woo+bVYXHGORMO^x7C6 ztesnR)izbIf1wwPrhOGK5fNNB1F5zE7`Y z|C}Bjla~0OsLO|ma66y^f(+7=t~*HJ20#ohKs;~$I1JXPT=o0xD3#Q&gpK_ZooiOy zJ4~IRg`NhN%dHzEsW~jCOrZf+-yF^LA*u&U7KfTBmHjD`L8?nd#ui-<9t~gIadaiS z!;+uU`|5ACe)3G8HzR7)FBnz&KMH760YvvW=1$9K@7&JRYllpzq;qkO_fMz#ok3;8 zx{T>40k6TXVKJ(oLsY-I_jhjx4s;GYBjy&;T!jc3PQl1V!v-7xwe`D%B<|M$rMDXX zPne6M=}m=!{S?ykk!Ss zpz||8V(m*kXXFPm#AEP^T4!UnS60+)G*aX1A!{+% z(_%u)J&=_0Q;2m`P89W?`C(P_%>ovn=!Rn@OqWA%q}3^my7O?l)Bo%BY*IW2bH7%m zA;WeFopkWUFl$SwetGbtYNY0+`)&9lE?N! z#t;sM#Q75O`lv;$+@~r49Vkh&m*dxA^WB`r5`T8RTo*7PdNvS8C75hl^2zlRlOy%~ z?pSrrexWU0`qYZ9zTB*KKz@E@c@|r+M0Lv_QN97+tZ zUH!8vmM3Kea=6!RLbK$YXzvs2oQRw%WpZhWay~OWJR!ISuCqdd*t3u5zx?q}-%mM) zVF8!}w}@7g#&3$Ou!XB8{m$}ziO4c9cui^LG*E{0(lJW%t_Nk6JQGi;=%N8<3+AJI zVUd<~v(MD&ue-%wqz$Dh=qYm0^5E+9RQ=FQ1;-9HYBENoZKxNIm3tO{(hwQi285}1 zL{25Ux%%eksIe=sjk~zF2ni||OHwOurB}y|o_N)XSHSvdQfz%?ulB8y$4X4*8iGdx zjrP@cZW0ko#h(A$cdhS8gW|DVd}=`oXR7$KDzLkraM`_d)@U-R`aN|*WZhlwv_LFF}H)qV8-X%jSz^P|rDU&cZyhHTZ7El+00~9TVM*C`hOOb5yIIq{9#*nH2kr<5F33f;;O6qzEozz=N2fm%1ok<+3ixdQk zv#5bcd47eWHh{==41*IIWs(T?>Q^NSC!QDG?9-8BHioo7EXb@yaS~#RgN~I+m1IQO zHy_Q`x#g^8f3g31h-kv=aq(YLJh>C1XEC+3$+P%&jcU#&Qpb%>igsOf+g)Gib~-Ow6EnQ6L|-yNX~ zoAh9n{VDPP5}Efzrnxukqv{=&IFrf3${ONkQs-;d5q>}q1BWP@B$S$~pyr?MU z#UIVZz&(zf0?-!zF*6|NeOdoDk!{usR(aWw;1}n%eEaeo!ysx7W^yw`zXY;_*p=Ww z(7Z25!ZP;Vi$eA%CSEk%^shIcEG&%j-qdCB; z;C-y_V{N~v`I(&*kH_2csx}T%y_ebk*sCx^kr$oF&TY`{hl}$=B#{y^ZQ|DMo z!|4Yvs_eclGnrv6?6wOfO^aNx8jQDP;mu8WC!<0PeyW@oCnotB3@T5|qZ&oA44X+7 zG2s@WL!vfLCItE?KITlI>*AYuK*?(^qAh*e>7Wy_Oo0K96%?fMsA$z%pT2(VEZr() z0gGu540#MiDU_$GAleYRd!eCR_>`Z%#rtoz-}164OfrDx1PJvRoCOG- z3KjR!H?L13D9pbXYh=zk_7}@w37-i0FbL~Zmu_)_c{H$dz1O0T!h(-&j@+a7{R(FB%j0*Us|6S_BSROLm{g|iA%v8 zK`YanQom9zV-bDUqB;58xWtj}=fclE3f_6?iG=Su^5zpWRrP9H6uEaC<_OX393!eE zCEg%4WvcKb`iRE-*w>0@$W6t6rcFEz+7Dvp&|wPg8+EjpQTpx_+$oWyPx)NCm5VTN z?Ii-n+h3APVZC3uWN|){d1m`9qe@~{vRv1TA^tb*Sa?P4dx2G=brTVD}S@+A-oNG>$naSgoaQFzLDcM=W zxUF1x%I(XIx3}FR?e3vE1j0&-b^CXs6?(ytM*kY(=(qf5q(8D{7Jt zWoM6!wpod5uJ{Ag(>s%`vs~tmN~hU_EM;m~9dl0-lX`6bK26+&DAW64*xuc#lH(w$ zJI?1{(t3|r(~^!C*8pC1^3IWQ-SM~NjL)c)`m`>Wob+MyZnV|{^0YOp5Fc!Ds-!(% z(Fg93*wl-bZp0-!^i_v^mtQAh$pVB0Fl~GwAqZL1`JG|PVbt|Z)Xaxv_4WY!8fcPY z*Uz4E-kcW8nd@e&;gdV76M2KXuq~v&z?HkJOn-am308 z29N<~kDC1|3DLvwXqvbGRcE8H%Kond73lx>NF&J^t!NGH@3Fw4W|`joGgVnuv98qf zSyy;)`x}!OST%dcxG6C-&7C1T)iNCKKYMGjC(3OaEaZoTyNL0V$56tOvX#b`@kmr& zVz%j+#@GIT&S3eZ{`gO|e*9Gsgc6sz4*hy*`&@C@$u+TZC{a$9<1>5m|4dX>#d;LP zTNAYw_L99`rD17hJ?SbEkBm+qIFcSPT8a0X$k|vAI`z(_G8^2+8d4^crcG8U?baM( zI3&`jt4cx)5qr24|BOwv6DAV{oQAr${fU%vG8cEgahLGBBcnc#e*PyKU*|6N@wZ-` zvV=GZGtFQNggrLwE!t{e*{Ee}R@9J%{f9<;4KlBQe`7vd{24y|tHy6hL+L4J0ypgq zahV*#^EmiZ76tNGp8!;8!*z8uQO|#H3_t7%cifC{U2|12H&z@-(fS@r&->Qr9NpM@ z)D+0sSh%GlG)$EVcQ=*Mopl|<19N}5DaG`v1=}c1zxV8em*aZ0BnW$yyW*8NS~yc; zKkuc_h{ryUL9H#0Q`$=yLjz5P7+?&CV*%8sG^=Tx)M-Tf@nhDGeH+3ueW_QDT-;e& zZYK^E&IR$%MFkz*>F|2GiuDS~HheGilyp&%*69mJUab}jthpB1bRth2yACD$GyEf~ zRkv==fBFktOGAT(2ChO{rmllW;nN6KZ#4T<-=C-itknN&4jNp16DjYHQ0y5}0u=r@RTYXj=?%a4nvqku9IS=fvl@o}AxQl5;1vud}(R~VK zRgw0fks>dgRn1oa|FNQvkXhb~?%C_96N}f)gy|!s~0DFu< zUP%({#+N0M`B-h5{c%{K@_=N2qv((8No{8c8R8WSyCQ81>79-~hDXvoMv|p0u$2z? zw5C1aw7afgw=U@YLXRl7gz<_7@H>;WUGBzxWf_JM+AI3KlZZPMI&=tkll8@Gewt(& zHvbvHlWk&kH)bo=gH8^Moq?=JPh2A==43yZ&rrKG!5B_pNeR7?A4d4k+oM}{5+wPy zN^JkInMHIah|?$^FJNm)q4^fQ#-K9>rr%qKJopY`)M~vSh)~`iln?kUNbdO13Q`Yy zKAy*)zn46q`N*reyTx|^kqTFI|Dd{<{he1YH6N!|=Kt(t8FsbdMrHeOXo_!J3}>J) z=vy9zg;wftDY9?L>tfpwlXzMs9lR5z16co%sqq_Rc&F8@^xZvUkTK0hT1k2!S0t_1 z%S8&2p5m1dPDq_@sysuX1i`L_j74r+} z7WjbfD}U4SU@Aju$@hc^P*x&!4~F=Rb2S|cz{M#B<8S1GGc5I7Xx35)>7#um@lK0!Q{)li9t zr-b;mfMc3j@{0<_iT?{qO;WEK$Z#_6loZ~t^f;Ff@*!7l`KvEOS~lbGfYUfbz>)Pp zQ1B8VWzZzXa?->L7yv{gf?Oh5DB?W*3()N6)@tg>-l0pFoEhSGLHVLK3xA8*Jxc$~ zHTWL$q4M-zP}Xv6?U5cmJyB#$+?9DkP482WZ%^OJ&_v(zoc;lb>%F}H-g985#GuXP zyJIdG7Fb=~PsK|6y{{h9J#mi>&-2gij|gk-r#O~@$qgLyffjx6ceA3Xay#OQ>v2ym z^mZ&DVc`D2G@|Z9t?T~79lFR|2t63_0F>Z)xMiAm9g63-?JgHPK~0^){oV5XucWdj zf|Xr@?w_|Dh91_sLp_Im-4ZWNMwR6XMi=Nt@keeStPkq$ElmB^NW|P=L55-oHFcB= zX?C&EZvm+w*bHTT1VE+28yD^REnO1z!bAU)y+!Z{jX{A-xUn&V@Q| zu2|Uy7BHTMLojn?&It&mP{6DAMzqg_`&vZTdQ3RIq3p@V9ZuqrL=f7vWuR!}3hL{f z6xxi|eVq#BvWPwJR6zc3Xzf|^sxO7#q&+8vM$hTeQ3wxBU<5StiEGyG8t-QEC9>yw zAvIf*KEt-UFC6>0KY_;ec<^d(*95c~IK_YT8d?syOM2aLqj$a;Azf=z6{#4F6Y?E5UUX&VPB`xNyOERoUGPt7T^ z1IMrS?^)0D`aP!o=9b^o{0}BHN6mQ2fCer(PDw^7YwGOgwK-`C(C zU*)6t_~vwqN!l39^IJk(R=>$oj%imaeJL_+1S}}KbuX#1bCSetC(xsa0V?% zh%5F{48d8bmAagWAB8(k^pI8{wo3x}2u;DGh@|Dz4+{wmzZATWF*?6KDf ztT2&yu>zK)Gk~0m+5hNLd_0o;?V?APb33Fb=5vjqiQ4n{(l_JAv(BYK*7b0wk?aCu zyYX~-%D~C?Q_4+nocR}5xGo#eK%rK^>%srac=DgotWD^M&ZSk zB7NH?E^)rj{?_Av>C7xS)Y1x6qN*JFDa1e3zAE_Q|8h+Bi3dVEJ_qN$-RGRYT9-wH z3W7K5EJWJefB5!o?ffdjdF?Dp=9EUCyu zE8C)lb`0*#r{~=cHKVFPKar3G@7c(Y({t}i6`0qD(TE@=Kb1=W{LAy11#k5EWTCQq_^cq>K>#GuU|5wDYBo-3Y8- z`ZlB@dY?tSUaUaW-q+ei&;oDOZNLC#TZKxpbNfMbDuDzWuxj8yq0ZOk%Q1>mL|krdj3j1bnxdS)D6VmnoZPu-a=WNS3n=;a zeS@iXhn=uxQNDY_mJ#hu8!)IkR2x}6kT#{g zuq;~dx1ic^JV8#f8W`bs)Nl56IUWQU?G5q2QmiMZ*~qsP-I&-P>!X)E4j*Y=!vjO^ zexHZlY4>%C^f`wHiY=}cAbfEfg zf89ShyMQw*h3jnCEd98#vzeED*dgLyJ%aJmwV_mb5tVIIM3p|`gW^A)8w))RVI8Jg z&p7$-tL3cIv(JW|#V9!}RJ87IW*y87)c{XlbD6b~s-mKMoJns)Vztiy9Npcfw@Ai-N zD&=Q*#S(}jTD~84&U!c9NDfK(Jl+o?3}91itg%6=n|fWEnYPVD-aM<8{+geIz8q@V zTKF1Tq;!T;7V-nZ>S3W=W1yaS+`h0A}I3UODO zsp25`vKR|`WG;*bCVJK`N+&c9pLdck}-fuCBgDjAr)%`m$y);KCtH3(mSG|;^0_D zibpaA_4KH0k%hk#Hd=w0L(2_aF~83?-+;rKlrBt)u#){oGPef5WPr&b!!TSzq1K## zU&mW$KN-aU$WK2eut0`1> zz}PUq`Qq*Oi*5I1I%yCZc`u@Hy?#a?JTch;y(@#k5H_E-8sL{2Ia4E~6PLN(hC__O zx!2=z@v?K{3ulzec(I0w0k5G+%|t&Px(PtFJ!bUnfQ4JzaK4*c+_iCsnX*xGpTf3^ z-F#&GEm0rm^M@warP|_8IGl-gHsquCIO3ReKHz4>wEVlj3F`@X2ih~ZJ&=8gD81!r z7HgUhEQ|!D|5x!qW}fc}7>ls??U2a!+@bMKE=#OI3yeD(g$3SQQvrfPZj<=3BllDg zF7vo*8T7L!zXws$MyI_SQ7@Y`8@5{$FrpCJZhmljuNu5|v3i0Cq zEEH0w8ooKJSHsW8WOqIfst=m|3sEGr|1$Vwb8Oy;y-rNfAB=j=+zY4@%?dUl>VW2=}hQqUI(s)80wUC;O=@EyK1p%2O zzMPT`CL`(+inRT2l{yeZQ+&s8IbaHn3OhZw#4O@G84ldBzJ>;Z`cf25{xV(XYk%?3 zP?eChDLJSo=;45@j6s^J6TyZ#bAd%ci%e)Rs~1M6e_n>PFua_<0{feY-2kBR(&#;d z6n`c5B@VHtMx=?5oc?uhaCfO!vjEgm^yjn2P8OW)$HZ{J+MNK)q@_uvT3T*y><$A> z^pn*^ByvpyCdZU(qtHl}+gk_P&Q;!s`<;fk-%%=1n4{nAnE2wLKi^AWQ6~;r_Cn0c zWn0}>t<%SLR|NulI_D5a#5* zq{T=GImaIz*3Y-u(Ab}cmK=P`U)IsfD`?`3kg4_^YITUp@@ zX6@Ef56^*bqcTS?J@*%ql-u1RnZRoi^{@B% zGY}1&M@p-m&F}_Bh4K0&EL70O3tCYZ(hLf6tC0$j7&3Wo9PFk(l2MNj?l__JYjlgO z2-V;}yNLx6tg+lOS3g%;^CiJG_Iht=%V(XEof*X=9fPW&F{Sn5Tk!`G;bXI+vv1%} z6VEZ0_Hxk|iuBaAo?QC+Z@dE7TWcpEOR4$DY`G$tq@UAxzLEw|L}Uke?G^da zYnpcI#IfVGb?%KMJ?$-%$nGTBG31WV+^6UzlTzGYyKIlvK{CQ=T_~l?w+#U}c=P5u z|L+sN76aZ6^bJ}>1*WUgK)|BE#@rkUGCq{APfZ3Gc$7K8m|9uH78ud&Ty7bgTQvt?ube)88X;oT|Xc5*QN{j4C=XuL+` z^S9AR)YX#PiX8*p@;hdUFnH6_m!7^SS8XMhWnx1u+e_P|=yY6DlCCbl{UHMbe_f2Q zrP3qn{j%~|>fLh=^T(S-J!fgif4>8|adk$nJWl}7aN}6hwEYrkGSO8ZCZ3k{|L6z% zFkTK-`upr%>xh#~#yYRVse}SD$_)i+ShIaFG~4gUU8d^4X)PX%C=pF}5E`uI(Az`W!O~ocL`VFCLqBv+lAT7cS9JwCoQ>yAuA&AkjvS$~jgD z8Z7$-k)?8*4ECkDH2fQ5bpvXuR*CR^PS`E&&5j0Xq&Xwm z=G8vK3Ad**WhB5Q#0r&(ulKb7_n1FL@S#rI^kU=GUpbmW$zVX)}q*H>kL{&}m zJQyBFIkeU#o#Fm|Q{?t+qBX1jgSb@T7`GqjNpjHNQz$G|p>?9o!FOOT=uO)Srdc~} zs=@<)1^tT1)T^NJtH5Fy@j0ui0S#clbJcEHQZG7+AwBltzP*MJdrsxDn!S8m z?Xlx^d>d`#{a2cA!sSD)qUHQ79UyG;$5N6i8T2bD_e%?xnrqkj;GG~-dET`^kpxv8-;UWji_hw4K=Px5I>?)Pollk1U#qQsaT}b~*AM3-hj- zNR|NG49L6PB}%&{yv9&+|K+Pe?*a*+Z-iH)55Ox4e(pd=l-KF(qTn;8o^wZ{DHfz~ z&96z&@rS#QA@rZ@c2Za1ZSgb%eE;=<_8%UpM-k(4wgKGV-5!Ir$;x={oX2_~-X!7J!ZmTn?UgvJn%~d>m}qJ%`r7 z=|jfSb%L3EJ(>ot$%pjy;n#u0hIKRAPHx{Fe~v6><};ng8aI0hLH7&d7Z!ddNI_Zu z69aNMAr8jv5FE{{EPn+N5@&b%AK2Jj4tHmK%c@j@c(6GhkPvE92h9EfyPb-D0kHo^ ziA@TzU(~&Mh?&yj_;K&Bgyce@DBZ;=rc6``0r&{6?(3vuR8*}18%;+EfBzrd zXVx5f!VA)O3brY(8QX3~y5ZfkA+p0kjhKs5laaSXh0Tc<@$%^#GnRfC;kbiuIej2| zArk{EyoZiVx8l2hHoZzWLi~)e-Y#x=WC|(!6FS(0<%D}V{Qe>5EA|myw-}k-{@IC- z(i0n{&7#aZ-6Kcj3jFv$S&fx=7RJC;{hY=l+T#JwF?63>N5}VxR0*4)95$vZ&^y@I zHiquTjja<>om+d7E32qS4G`REcS>mo0jlBZsmNDSxPn<$h;z%-E0Ffwp7D8NI~Apw zt1ERChgqC2?Q-lawo+NW9 zTb|TbYo{l$35wJrWmO=GwyjFhx*jF!-G6R#yd+1%5}&)pwY?1F_4!VhAir+#KXKL9 z$I4#q zLNrrk^qc=zU-h6c7I=5cYH{s)^S1f}VEJ~sd>y%>pPvW1R=l~vOxTv5oVURZ)Yu}x zt9P2v67z;A@L3MZvrcKu^{Z+THAs*Zy)re>a@(*G=IQk596#bd4&RWH! zO+C(LD6Xp?@l7i%Veg7&^fde;u~fDvop8%Gv?t2FF?xPsjIej1Cx`I_kY})!>hhIf zfXTdF#`{Y&mLarp6UD(*P~h=AV|;+I+Vh1sKQRT{8n@#|BJ^C@k)nLEYzFuT%q||b z@r})~e9T5D=CYaxdKxaH0&_YmaF3sZ-9AAGVN|14om*2AH*=F-n0$R#C?1a?Fxl;8`ZFbfSFhJS01tmgN% zR1%FlcJPzrFUTifh3(VxAa3)1s$NqbEu!A%2HBzPg5=g)-K{=;(;>g2?7qC&JV>r= z(3VItQ1+5eKObUv;xU;a;=-tPNIPE&vuj(@^15{ijF#1y^0>KATR4h*5Y3L^GA7IU z@|8kE^mj@i*;=sdWdoxjod{w6-c&GA9E0Qwznn6|#%CqDGU2dVY=U2K)(-UmK93Pc zKJug|k=3ql7Z3e`Q@@SG0`%s`%PTtrZ5(z7A$^S{f2fIgpW@Eb!A?Cv_9T(u( zNqT*y_J{22=hRob5NTI@o)Gi2B&wr%&;PHluZ)T#=(Y_ZNFYFh1`qD;FhB?vY;Y#P zFa&qkK|_KBcOM|Qd$7RZIuINN_aV4L@I1bE-&*h9KevBY_o}MZeNNS>K4xwL^YhU9dMms{j=I8xt<&BTiN`OS1m$6Yv4KL(CccAt|3dGC;@$~QG!t5| zuf5QhLy9$jZN*%oR6vUd{fK%W!&cnI6{}7ZNw%OW8leR^|DgSyF&k+YhL{GU!uEs9 zIk`A0w<8G7`@3|3X%LjfNVKtTOkwOK`D|jIk_$U0#!!3GtYmJ$AVhhBtyE;G@wrxj zoG;D}H~kM*|F~_piJslizk1vh-fQ7j?U{`?Tt{$}E!h1?v-zffb*(~*+Icw+G|}D{ z?-*93RmrebTkF@OI=ivdHCC<(I>O_xj)*dTN`5b}&ipu4U2UW>9>of9_Vzb;b8P^4E z{7eko*DIhz20ST=a(Ts2O5V#@ZP&=4yYvqgZ)VNA)jA&HsNT4jwrUc#m>&s(=imYc zKSe1VTCe(B-AJ)@h>zyJT0Q!aWyi%GHG!c)^_Lc0_mq>h|-E9xgH}ACxxLm|nYBU|u zHG54Df2wz-d%V5YaQO{*)s{#5>w@MT?YiMNp;f;avB(a$GP1GkuZvD_BK26mS87`> z(=uXsC)NK{U*7W{cH}R)-IJ>I74Z{RUfP5Uyn_t~AJ1k&dK0`J!yn0vgVCCnID+~c zH_pkA%`W$T1FuH8_D_xnj%6M)evcP8a=r4|q5Z@#=DyoBgS)+^-h|)1(-AF)+|C1y zS^#cG>>(7#p}tZQ+$u8zzu8Yqxz#_c0k`e z+;WKO;?Nhb9sKyM{+smbSWZX)nNchWrRV2cJBAZNACtb0$FC`hf*(TK08oiSsb7~z zkpxmuj0RNA!GgS-^PeAGX#fwxqvpmZF5d&;id6Ygk({y48zXggQJ{IMZSN16wEJA{ zGxM&C6_vO5x#zF9*fL5_qhyTvE(iyp)42Qou8YNV;#SRL4K4%Lx) z53+7D#)VaS+?-!o;r!vQ1NOz7?}1YLmTeE)&%uE`4Bq}r3a~|ZF~B{c>5%bC`)@lz z{_=$4VCJ{8Fi`1P^IFD$Mn?i{vCLW2v3d6mc=8|cez&UD`!YS;8o7ZjR@_K=X?WFp z>~j?-KjrAYfA<2&_|H=FQ`z%d@Fg#W@|WM0t%$t+7TGN~N&)A6+Q+YtsnTB_;bZa3 zbVqwPvAIhhSWzGW-NG+-KZsjPYDa(Xb)OyU)U5`c%7$)CcBu72Meuv>5?4BR=+GIr zZVgB8pOpPvdj3WCRQz4(@8w{@4jPNLB72_26p~9H&BwB>A$v4mzziu!IbiaVpr{W(JsIyXzMLi`;Roe zOiy-@StG~NIT24hTKQvZEW8$zI`^9~xZ{Ot0wab@5>j@g`MULutP@xCez!NOORiAk zfgX$~wR_y0tQOaGRefErK9`OTTH}!b4A5Wr`$5vddi84D#{eXWx4om1-~GDINLuT& z#nqNq1OOJP#jIG$ScedH7RZ;=je4Xp)i>Jhv99gO)ck zAdOsw9|cgj(~hn$M*kUUPm^>Q`OTADKREr$hqd9I^$VfbT8rh!KIe&k`}Mrq4}(Y9 z`u22lwet=MiK3*X*mqt$7ukrpJL--U=;)yy8|`ZTqqw#gIokO%w%yZv*+vuOuu_BX zwJPzVUN5=Njy5o=1MlFwfVx9iN81TsHjEE<^U>=?Rm+y@$fsuf8A_U%%P~NN)qt8; zU9XJye&D~aNY}kQpPPiV+rPei<`>VVM>dUFR`Q?cXwYxdaFVsQNwwNAgs%9KiC?wS zdKlzHL5`$cd8IDnm7s$~}Q}rEx2< z7yOXRwQ-2_c>M0DKRD#(?bQL2MjAN$Eek%4{({z(hs^X2h5b7Fq3Du6j{!61xCHi;D<6O&h0cc4cl8*d{LH zV!XWWX!H}zPN*xGh35UAd^_>^YWVeQaqwM?pIH&h9_iWjGs^qxr{RjNwa0g>c}<6fAC8GV6WhH& zYj&)wwDu&2M&^d|C8nFxiqlR$$x&(8S*A5+iz z2P|Q9MSiV&hDq$JuN)S0B1j|*2fgeUeDbyyc~F^1Sqw3Ej#Gxyfqj~O+{BK~mx>~u z*DOWw2!+GZ&SeoqOlliF7`_+p?RV`^!sJ3Q{)>-$5DxZC^$sYBxyMzXk`n$zxK5PO zNGqQU+(L1ZS8z_OZtWc+NYwJx%~F ztdVb>{`m@$s>p$$SW2T;f}QhLIivuJWjcw^i(4*RE6P5HwpFeTjx1{3KOKh=Jxix9 z2^*?ubZi}kxSzK%OKx?N5LU|@NuXVOM;C-T+{PL5=HP82t8{e#&(*k=yiB9u96*eE zaL8g2z@)8SDK1!mEoU45?X4yZ)zFx*G)tYeONg2Rdl(9##jbMq=`X0>F92_oEl}Dt zaFDeT%Fbx_C5!5QV+BCD$E3KuR@6{0K!vRHUWx_2rwy6ZkV@=hs}588*C2{2UJ>+8 zF^f&edX@iS-^AB?4_{yI&7SLkLN{MZ@5%92U{Wx6L7;C0*_j|IyoD@#nQc`PLqJdX)(#*f>b#Q`z z3XNe4Ld;O)u8fW?tNnQZ<$PE)zI@UE=TSVKis_*ci#N(%wT|z_P6-pK^<4y^P!F;(25y8;@5u@Jw#imf&`Y*FBX6$RQdc4cQfg38^X#DRsn# z*DmI->ZG<^t!@x!*ny@l*UK%sefY%rZ+%nRW_7Hy!2%Zq2(cSr`NvvMvt2VZ2GN za2h0mPHM0`bsIOWZ{~urJ?<&GuP2YyfGUM~t zPz~Mr7r~cUase_Ht!|N+rG80;x6^u+iQwHNU6f7M!;ixnzlf$YF3A6zLKI%18O@si zkA*_lrbiXqo4RH>W@%`=4(rrB*d)TO<(Gl^xMP4~gfOU_U90_pZjO$n*eMa?cgd5CSOVk__LG3}sNW|4 z{`^f|WSAq0v*I7}CyPn3pG;b{1;VFSWzx>aGvx2wj06*-=%&)nhMb>Q+-kiAJF*L= zAGvf``_p&29+#utAef!yx?3)l`tuUc@ais?D`JqM^udB`Jsykul%%-sWlnP6J=Apr z{2u5C6gwrd*ef}KNTBvJ3oX_Y4E3LTaY0ZooG~&PT;v&x@)xr?4C%?W>j!8NIVleGs4da`dy?hc#2si z8@^UK+~HR3vdwch$7NllL_AAG;pRe*8?T-N#2pxwFFb$o&>y;elF3hB36qOVzUU#K zL?L}Gyvijm@WM0K@dCssh``|K%q!N9ej^~)o8{L>0k4g_@w``^HP=b8+t&f2qaVu` zYL>oHnjU;Z=iAAa7s-{Pxfyumiip*-1-q#dy65GVI2+x`JdEH(zXdugQOaGlCz zjs=r*Fn$-bp83azOd5QTSrT-Y6!oHX5%LICF##Qg+uEotRdN~he=?3J zC~y3&#Tb4LZ^7`Sp%jyLiwU`MxqG@RoTieqkmr@gp|{uFccwbKzPrDEn?;fSdBF!cO&5bN}9MF%cn zRHnu_)5wdgo7C3~F5Jbri55j6`ERDYquv+RwiHHll;bn7H-0o==1>X8{ig5tZor-X z)Hbrv1W|%0Hl?H-(+W>J>_x`W}7U>lZ%ShflNQt5bivUL2I9FZOP; zORxJlSkEV>=4k@Lsx~iQmKY|s`R$PM-P}m<26oxP7F)~g(0dfDTZKT`@N{1RpQzpq z*6m*nmMHl&et&U2mCmv;D`4+a%Iv#h)qc#<(zm6U-mir!HI_Y?epg-QuUgxsW7gvB zlmMc??8A>dy{jy1%5QL`JGX3TH#03%YS+X3#Fh+ja=!QM3{IYP33D2 z{|>!8`&c8G;vA-^9aJ>*el93|GrPxu2fdhT5nNL@T&v>$+_7R=+Q-~T&ZY})O0SQXJ9?UTb4*ZY)qIv)*nTxCD~$!WXp|cOV-4A-^$WJU=HMw^2XJjTJgU-&6t8y!_@jd0|LxM+&hjSoDYz%v6YWFJ&*1b3;1tC z#p`amngZJm=xPadO1Bo<^>-kFOsuHPkeIIzb)SP8+(gE!pt*<_|& z=lOhB4{j6i9zGyG4?NM=D9G`eU=K0$Xj9)2C%d`N81=~CCUzvG`f$%M?@y@JLXqmn z(F)uyn!IP#zft{2%hEI~x_i*>*+xlX_&gs%8Rpbs%;0~He0L5jcUB zCcMc|k<2H3ii&T3EalnKxUL=LKH75Op9*~8THE3{V@rNK%IowQj^!{l~Cc z?5ZNbv|9I=AHmvlw%|&YIB88IUz{lJQh;k+Zw;Y<^8nUjwMfw;6J)if4lzWtvN`?q zzDzvJv&ZY{_NXiFs;i-kUU|u@K-|RYlLekR{-c+gl5QtcH*}Wa?2EB;BbChF zd*ys6@$#eOl_`u6i`Zy#lPv^O;WuI-3s&obHVG z?|9o{3GR=BAe85hH5QEDb^b!mQakoP>(AgQLcTHZU9HRPET@Z98Km+1CbUgxVMr3h z_ozQ-GpeOqxkbwExOytSin|@wmP!%5OY_j`)lcN0E;~PSY#Ok< zx(*lMmW%EI_690EkXw05$c!pcMd04uQ&_>K)jWyYJo5UZPjV7+QA$>(7bltHl}2tl zCD0e#Plb%)9%70#t>XwP@ZGrg)=Hp6hKM-kaw~Nta+QiD?1|g2BANYq{TwvDQ8O@p znY8d$B!DJ6UViq?Oy73Iz(#Ht`uOgS!uuN_VT(%?V6QwnBS7?uzs_)WlT)lIpDb$0 zZxJzcO<0%4uD#FN1~>~GFITY>bew)g4cre>89iWkDuA|IFSih2UWigh$JlY9pHoG} zqfdK?U&WC$r*Z854fMt-$29(isV0%9+ZU^`xg~9S5EoFjuRKUsZ+v;c+Dh7P%J-w6 zjZM1T=>ij?(CFbqA#t;uK(j1I)+7r~_-tN$krZ&2blz?(Yu~OxN`c~*RF^8~KcoxA zOa+nJ(S%xApb74+-?^BUGmN*K4_%w=rp|#o@E$MoDDlZ9P6^eg>kD`h4m4gxm zN{ioJ_~rYDa!0>D20{%K<{Lv>4mjbNkJ7{P1Hqa=PCZ%H5BCDG5xrUv(B(#gx zTaZ>i{LdGjCu{32?N0XM1}l-)=O4F|GZc*t)Ec#Z-pT@Pxtl>$D)cnh*F7r+nVWGGbt%HIGGJK64&m;0kwg?omEG)8+>Lh>9Y*TkgbQ2Og^X~E|(bGeth>@I0TApL28@TaUKO@NqV0eD)Cg#U+^&Cs3Y+2f* zB&hOspK9iZU%DK%@*mQW7d^G-!+qRa&G#OoYsOK*2Z0|I)TSzfn&(TNNV9cacKmpRC23jN=b0)`X`L% zkpG3hOrXyp7hAK|W3*@GT!&irz;p>+q>@^25qEH6LEq#<7Ey@;D$>F zCUhcmi~75`^o0deq^iJFVwwO=-o2=AVTbDs%>FuW`sHT?hM2g?eKjE}UDJWS8yr#* z8?Y|*H19+Q<;O=IMEsmEELP~fxk;mb&Ou;~QXJ*vVtvsjv;OBKHbXSmB0bocN|<3X z=aE8-vhJ2~Svq)}_w-r$HGNW>p)v%8VDtVSLrlU>(Y9n>Nwqifd(%*6MgR5eu3VKN z)U2M(9^FQTHYsvXNGnWP(x+N2`Qc^?=Sfuc@!t9X(k#%Rx0 zTmD7^Vn8S*Uez`?8mOww!PJ#3hhi^v6Loxp!o_Z4s)V;oQamo#em90^pSUL_g=p#C z6c61Z>T?7>K_wL5_}+8M{Mr0+5g1CG`?f+EA#-#%55jpBYE0#dc0 zP>Z5x>O_b6JU4J!h`Vrn=5>inLboodUqs}oTbk$L8nT)N(XB-)aMoZ~)JaxF`al%C z8LMDLaG8-EzY^_b6ELmDNfh&8rq~51?}U^T6q5)d&Tse}%mYKf zs4Af=rD+d#h7Y7MkneF+&>lfcQ4IQr-I(sdjFj(Yd9M`zu;XvdkkE9I%~I~1&Moth zK>&Y=?sM5N1x7&1?7<;V3J1CSEuxiHsyBKcyRxANsCw@IFRwVJ#NTX&n02%9j~#!~f9OKT)n0@rTu&+e5na zO0#Hk@1t%uF+k1Dor%M@VEO1P>zsPo@SfhMpWSeDk9--a6R-Puug&3}7+0)<9U&z@xU$Z!U)dpbgxCWw#P+NN{dQ-8iDu zc`Mi%vX(@hq6GW{6@Y+S{Up$RVn^nW+*|A>=c*DDKQgQ|PBe^NR4|<|m4gYT_c15$(7BR8siBn{;QBhjkyzm5U+9 zkzpUpN7fklvqS-j0WUz`%gIP)Wm^~f;a;MCsK@+rsVlblPiaP8I#sfFSDHam*u|eH zgLRyI3N*!6UZuB%SFPo7Hb?jepz0rybsK5r4mkNSqRl-M_{Qn=Y#OQ+sTG`9Z!)Pk zr-d67-6m}%B$duM06|g$Ablvtg_tFU!uHqji!yCDm1OSf&*<~g?D@}fYDGH3vG+{q zY5&#xI0SNl&)+{09<8~5=i^w0&=Kq6)VFXMw#evTS8$=htD}A~9BQFSb)4xNaB;+l znW*ASxe_m}tG50{&DEQYFDOucO$w>rQHx!5Cncl1?|5w-&sr$40Q$)^3Zg;}XnGl6 zEKsw|Q?PaSY~7_sV^K#jt)uyJP+rX?ge~W0*=aAFS_p(`pVmSvh9^52(iAa z%<$H(C*xtzTHg0LSpy*P{lHKckWoCSAl&Ob_P&>KI7cCmlP*BTX&6C?!N85@$ntq+ z^Sh2Rd#rtao|(PMRN>DILw_mG@iz>y<{OTlttx16kVnVUi+Bzdn93C2Prs&;Oy+$O zl0sqoQh%~hq7lOX$f$HUvGlDdpvT#;2}K-ceCD6)tVsEF!t?eWcOL?j^17q2$s(Ed zKjU!=*QryfUW(EuCm;lFgqDP7a-qr*Nfk7D%b%6=odrAI8oTIxC!qHyaa9qXiHgkF zU{~^8WT50A%;-gzzvv7Q>fibrwGhu~ns^lj@I%+M$$hEQb*D@7OW&fdve`|GIo$-@ zD4qm7uYo zmfq&->?^mYW=~}!BNNR>fb%==&pcx|cO^rx@q+@(;WB=ll;7Yayy(3_;n=!D(KcUt z7DIpK)ZKUt8=QJ399j%*f8kX!|IesnrF4gy!y1cZN{rzmwfXT9{NK3yEQNP$XAi$Y z@|Q9Pc?uGr&hv19J46JGG}pC&8n4G^wTbEQBQ6+MU9&T;n10wUS9B}dwH+^;T>7K>ku2+}iEDpO_O_-R-cH=+iGCjQty{UT;0)RCnqj5D5)iRQf#6l#cD&`WypbSDlZ=o0 zID*i$vDr!*3m4(Ag$nl!j!MIyY0-Qrrj-dCZd()Cw#G;cwOQfYWF-p&SW2a&2~@M0 zHyUA8#m}YcVF!FV(v<&XyWBizkFj!_KaQqoVTM}H>EPnCb_@cLH|_vk5AB4Mxq@lK}!Fgc{+bO@zI{8 zqs|I%RyS8piV?SIPhe^JE7T0k8O5YZ_T=|V>%H6W!twiTXj0@2Li8!%`7W-J z2rd8ML!S1$N?!)~r$zRM#paFrs&FlEi4v2MpYX{xSTnFLD7J)3QLHa}`_ybxAQsk+ zE}V*8`Jc-;a6s>i3{8V`CoV;yq(=xC5rboQQU>M~M~bwv+gsWyiIzt-wg_VrjBB}$ zEHiPN;PI1;t%jD40(v!N&!SaByg4%dzbOv#aoUX1{e#Pt{HAigYY!tx zdE4Xm8aOL| zFu}BuPGfG(4I^bnP9w{e*4E;pR`Q2^a?GtAyPfU~KW+t&IS3l+Dy zWBbka=P+ho2s5LR^WLm}(aQ)w_C%<#M{>DekmGa4kbSf$P~AOkh*#8OY@cHC%PF0t zMmBZx>^?a&hul{U3<%oZatBSn2nHhqocY|gjN*xY9nMNFtSvtA=WT3pIYaZqby4t- z>2%r84#(PUSw|;pselL- zDQ@eFSiF$1U4fZoMTbE-%8+_tBHAjBs1_+gO`{AVEA6CLkxdpVcEifDZ?$3Eg>H%Z zW##D3F1RL!71S;kdl4HP)1IpzQ8R!{47K-^xr=0(Df+j9a(wJbeMt^?-R#|s!TSXS z?%xHz+DU#JWhFO(8#MiFv_It}Yw~9ggO10TIqYrlH{_IQDt}~t)5Qe0t-^biYiv6vPayHoc zz@&CF(@Xh{pb;9;@qYG03!DQ>A$KCL7rC^Bf7Bu^DvRT9>Lx!2Fzs0u*Rbm2xY5fO ztD%iLMQ&`^Gr*Z=hJGO?>L%yF76$FS*_W*1s0r|m4v$-dnu)UAddk?IR`Xd+mFsix zi;?wSCa?~#egGEs11rP<2U#C4LxCc&chAcF8a9ooTUZQ9X`p#svo@^!?S`ZQxj{_I zq+$ZxisikkFfu5@Q?-q3R9{yn8%=(Ef0=xx(rnaM`Q$v);p=V9%5^Ah|M^K4_i(HL zc7lUrGu^`}@I!&e@0U#$g;RhiTqJdt2$`>ZeVPWjaMQh}MTiZ}fQok-GZ?hO3t04_ zGNJ-s_MuFqCPPdbX>=QT%Llp}BsfQQ&&UKdeJU$h{XXe2QxwjCE$ocWS;>{3%*zig zOIYt&{Ph=J>L*fps!=>Uw0Y%n!F3-h@NU|^^dviA#A?0qX*~~fS^t&4sP13bXj|pL z?g;s;*`qr(nkA71FuJjUfSEM?<_Dt+TzscA330>Z5gahCXnIQWkO|_@;%%)?vv?kJhirDEE;BV-;lrL124w9CP03PqAftlocWe= zXCBKURyrX56?Av2#qH1I26z7mw{lH57CtAz!BPuup>kgU8miM))$|Yz6=Z&3v1$Y! zPh!_)_YKVvD|~$O1o=8&iyqw&6m!s&MFY>Jc>rC^nF0Ta2^gPt_c-r&|FLO$=oblL zioCp!QK8M9dHrh%rW--M=>PwrwJ)dd>lFPv-E~c^|Hqvn^r1ZFL@z?{e{Xf}^nRy5 zO~q@J{5Ipi`v0%K%+KD}l?-iVy-5Ax~yEg7Ff#5VQjay@l z4)49+d~;`Jt@$x$t+UUzdbU^9K2;GaO42y5NM50!py0^LNUEWrJhy#{3%^8v3TM)i zT2N5132Y=JRAeP2C{Z>8riOWW|5RnB$ozle~=fY}bT_ zmI{+jTwJ9pH}X|E+Pim^In0#xbtu(ymCV$0x2W&lSsJp;sdy(mZ)&@2MfkbG$5UO$ z?yQfX3B)KGXzDy z>@v$2=yop|O{1Xd)d<)BwyQdUZ}$mo)Dbr?e`?r9%OYmF(*GW#iw6}OMWw?O?enKn z3{Hvo#MO!pC6>4m!i8MbuXiV2Xi9{N5X~NGtZ(zGao_pnk+2ifmQ-fk7}EMd{BPeR zu$gg0GzK8b@d!y}!k2GyDYb$cB{JR-=A8KH4B$nP)W4QB%g<1x@=vCE6_L1TvLLX& z*23Y-E6+<)qmYPX*KSpO%;#eGCeJbLcTEHbUVOc5 z>jPfP1K&K?`Sijo5bJ;-%@jgAAC3>)_(XO6mYNM8a*Z}11^tP#*B&|OWO?l1N^m5MC z)I*J}YmKa}pGKI@?G43iNBpLjo}Z|_s3U(>rqO?RbKjGJNgv2Y#3~tQcmtVV+m5Zn zq6qYrc!(7m>u?Vu2fnvjhun6-41QiphsM8lT$w$CBlAiFMx=AJtF%C>l`;Tt?jRa1 zQEALO(`f!m$o2di{qN*GW2OvMSUOlUFH`0~rI>czF>>bc!ejsf4g_Ub2MU-_eL&AQd8dQH)bYNOeCn&n zD$17^tgE#ASZqPK&Zw|p_X$*^ul^?#*win@#lO5HpAf@-&E10z5{JD8&=N&Rn#B>& z;;d1LOMV>@dry@wL#!5LFR>T95=S#2yZL-F0w&fJXS9iPM%=5yR2;FFyS+m+MSdYo z^T*5W)sO_@kGLCW)AOZ3t=#4b^mAGt%+?@}9QKK4*d{|nsN7u#9G)!rk$q}a*b3i9 zyX7ifN=X(YdAhH_IQv~UmCF3D3pNyBxHDEnS%EdD!eELvvWgD7Q}nx!{y4{dx2H}o zNW>r}5+}6Z-o|4JcVUZj=44bRIHx$H`}2u>yON}i6RV|c>OAHONs-M^G*E&`!lZ@8 z4rqb&xUn*{&>qac8oo{1@R%9)Gh+vgh(PJ z6O!0lMOJZ8VNjh>FCzD&yiNWjZoXAMvz&-rFoFAbwBGbL%7b|tB#Jt;lr?aQd3a(4Q<-wgf6k~Vs~oGWOn&+3ng(D^Oj6bt zE6L6LC6ec-kyA2P@~y;KW4lD7_+z1grgwgEp6&0w3PH^}l{$lqpL_47fjo3Pu63Vy zk`{#PY~wcK2fTCiVpOhNnG?j8y{4#U?psU(T$4VVRjL2gz-4lIN!XLEW{# zp}xVrA@+gLtNs<|HsDsT(Qt)WHCweOh0f@dNtUJRCF)7nH4zbYXS^O8uqw46Gl5xv zDe^^!0th^(s#SfZ4aqOWd~55od9|A*eX}od4#x!*GEFjBGN%Qj1@YWKhIUI4P26sL z`&ko*q-6pNb{~D+26s?%kN0wXW_+rAYA)*c>vnN=NO!{a;CoI}qWLS<9ULdz10Qwl zoJW4n#yakpf8a2%^MakAsYmXPk61aHWLaI3?h=O(%SP1`*OB6o=o2vz5fUGB`C!K7+2zI)~*tG5%a?^2pa<|R<)VNv!Mp#D@Mv_zb(nK5n)W2-_P+Qp0 z=p z;>>o#Aj4L}29a*yWZOX7#KSbvU(OnZBlJ4E4C}r{`CH2?V0}LcV&0n_nx=yubf}vqmkiW) z1s|)UHthB~@H+AI>J7_VTFkPD;)sN|`)@>P(dZKC-02{)S?OTL&wQU_Cx6JvDJm?b z3UV-dO*YAjBpA{YDz^#rnhlLG68_#(8qM-?OWXT6QyXmatv0XL(8gG2qSSWglaH5= zrjMvlY-`qi{6@d-@a~q6>u}>ZR}1$LR~Dkh+s>;MZ0s-4u8jYo5Wi&%dl(jvq+%8< zp||@W#OP~`E5N`C4_A>$p%WHaGPxPq&4ltod!dHjS>F3+H=kes$~2P{{tWw3fe8_C zcq2c^9)I*a`|HqG2=^OnvWz02I)9Br(nYCdP1d_mf(KsG?9r?x!Tv8}T>?w4_i)Gb zuo3y}S-};HhjpX~mN?-QTOP}Zo)>f=r?7Nq=~v$8(Z~xzcAdNH_ckPX zvCdZ$7&GZCw2gSr_gSvU@zW8?lgkaux3gHYw_lfRRFL|U>%~?5^2^^J8Z8Nb;untP zmxhM=zm2qXF1<#-@!;5{+R4wf>GL@(g`8Dw>CdRo$j^8>gv0*SPPlCpM;kL;zvE*< zWxCSb)nqNXsf?)E-Lq;R^=;U3Vq7}&E-~3ZbjVpGDIP5uZg|^3Xdlo-1L}vazyeAZ zi<)cP;IkyAirNnLhr5CeO-+VA%~~_L7x6dCO9`Hy2bc?gYiAvg>;0nq@^3r+2OkLA z3-0|9pL&93f{!q+D7b_)+*AA;^PbtHO#fLKa5}J+v)L2odXTZJ(MGI^9y}J>aM~6L z7rVP{aSm1W_J_t0QULhle19F&9&c{U*pyu<(PcOJ`>gL?a=*47%f=D#-OG4{Zopf| z8w^fboHu{n_V&q_CzC7~EZ`b@K*5bmb**dP7ZZQuLSnYZK$-pJH%21ftUC+y`)jl8 zOW{ou-rQ^3hx!Rl?n~P+e}9JCvHfN=PmE*MM&(B1vrmU&NQu_4>Bq{O{x-JR^4uk{ zB?rhl#ggxc|J_-{^m7v|4-}IF96c@$fcj zW!z%@)u(6x3MLjx_&4hxqm`**3L&lMJLJb0D%SA0-}s==ngrqZ@0Lwo0>VO<_fS9C zqOX>G`SQj2@1H-j5~2QA4Z)}QKiTZml>e&YW+y_eqo_hD;pAdT$;0-Mje{EaijtC2*u}z1 zP)$L zp(~{MncL}uy~=1(@LhzavnYNJYr>|}X3K2a1c2`#-J#Easo_V4Da|6r*=hb6Lf$E! zfUba`2}$+eB0c*a_hHm{AhH$Vs~gVXvFuePoBM%K&Lwlj}RyE z#}5^a=h=d#qR-`Q0s)iBNd$F ztmlocJ-iN%+As;=)EQeOT+&YPE8g!ASWBsJT)E)9@m@{O&AK^&ExK-OY-P}9x^9fs zjn0yp2mk~()`Ac88T|HN*yU)?M9^SXXb@`|nS7XHxaW>_dk!Y@Ye%dpg1tJ8;*j|Wp((HE0VRt3Oeau?aZGeB7R@C*@ZCi>AD6?%1 zxMNE2m(K%H*zgnN276Cm-RxIjJhbhbbKuOoTfsj@U4qG}SB7fci3*x<{U3`*iR!R$ z0KgdEzrPiq8LkLoTEBC}uz@eYtpWSIv)27v>v%0H@gMCgj~4TYIq|Q zlz`Gbq7;-mU3lZ(YN%v}d1)@tc{S8R^lVkVh@X5l2q$|Bvm=$zWP+KWC|8ib^t{)e z&AuEBK#M45FUMQe{E7!$;iNsKMla916DK-HiFkmb^J}<}39!7@1^^gtgBn0{-!FCt zBr@pCKx)WFjCxghSK?e==P21O8wHFrab|EG+1{4hldDAT2FJM8`h+INJ>kwQ+MCnC0LPjI3ExW@ZJz>3WF>3mCR&eyfFvSoLf)MxHW&=2PmRN^02r zy(ZW1KbkRhVi3bO$g;i#FJ| zZsr=4&a`o7#QkZnvX1o`+U7xsjb;A03cxkHal21wmsO zMg&ex*Mg&}uSWJ1%kyw2%M*H{(|Ctb0UZ1Km(; z6q{8FZ>@z9dhIT|C{pg(%J)Qt_pUU&F@Fz0(LC6kV4q+W^PonZ!ZPA#WWKlh|Y!=``U#Pz=+i!wE) z%JhC0`LNc2+^N8iLByg0;KdM!r_RMRnnBrTc&sqIrto6;0k)j)!G#I*gaU;aLTrQ} zeSp{Y8lFjyfU)~Ua3tN+ru{ci+v@)ZkZh}xfr|xSsE}CXV;^kH zIzH*)&XHLgZrInv7$otQ*W&`|`}%i*6x)@ctt#N0)-U?I$_-j)EhY-!%zS^wiG)kk zg*d3Z*IU*olwtbN5e2y0g5S#iJJ<>!=R8kAz+Tf)BUirZX)3Vyw%ah zUIZ?|%^kDL1q9vLa=NuJm1HjFUlT?m?0{xkE~P5u_ER_I5Rczy4kO&uA@;q!rmJC5 z8{bysHg${muXU^euAO*;oHMsGnB!8_j_~T(GJ6owHI?f8!ihd+M&kEii2Efc;ax15 zxyYelL~HJLGd?INShpVoWwUc?|6zwQ;w`647FIUiX&w>Btvf_fd{F&;k9iSVb9sSPAdinr6Zbt~GD=TnyZ&0&7pr9L|HxXTj$| zBpre28y(!}1}yI|fF~Uoq%9{j08moLFSf-$sbTJm7_M_fXBxi%+uEXdn>CVT-n{5W zy(-&f)Q3ELQH@J3jLMEb{}p+on= zaEpNCBd@Jpv&G(Q6-D#!AE=9>U-bdcbAX$AEO!IuEVn=V*IsKA^8un%Gokjz9nVoy zH!=ELI-OiiOJ1Nu$q=J7{YX?Iq%g)GsyHm8u?eEZ;{L5?!@?b1VR{|Kn?%<^>bKn@ zzMJ`YqIUy)Uv{3U@!5Angwncz~%vS)(Wq~ohyHxTqVW1=cr_83_ zJSk|aCxg>C!_n6y-F3AASm*we-dT$)MqvFeW1Q&16}UJvPA1#5WzE9BL2JI@Hl~SP zA=|@G2$YFN^4eygX1eM~8|2@(p9rAN$n{o@Z~q^?YyE%sPSj>U&mFGhJ-#F>MAQd5 z8q-6rg;~$O9^>-~#=&ftyA%tvAajeJ4{{ylyk>dNTlqfOC$mxVSnBYHx)P@C7oyK9 zAIo6L22(QYt3>+)hlggO5*8=Oj10jCA zVdfw~_@^+*m<7Q`dho&37D(ySx=~<_+Qx}4i@i*rn0AqbUG?}v#|x8jEW{TggZH-g zm1PP;2-E3;?%9TMhyZ%BnU1!8*f^n-OT~Ls{e0Z>AHxGS;W>b4O3|?IvqZxeDH#42 zAaavcjIppPKNrK%*=GR6HMpbr1-o0%YQr1;DHZ)NJd6MoXZ9U5UW<&U)ASze3Fk=S zkiWgp%^B!2{kWa(R2-|$Wo&`k-TI_B_wJ)$>>pr#LdRfvlIs0H0A9b}Kf`!oQK5rgC`ZtsP9BSHeTQKVq|C)%nk9QIGqhbXRy`j}Y=t z;gP@5iRtV&*h2-ZGx-UVUF~L|`b94mk}H@;S?7it)hX{VxzD+q{x^jGzaiM4nBr1l zCirYHE?B~qsfx0;k-QBveE3yU&Z~u7Vm5;sprA@;W;7f!aY$>wcUk zpMK%mZ+6V^gFCv=L+MUp!{1fEv*tW0no4{6ttX|X+;Oh4&@`kzVErUQj0zb5GjREJ zQ{nv(7ycMz9nyBc0~tiYFT&0Xz`v%N$0HZ%u-YA#Q05&sd*VaAyQ|<$ajv#lN90_e z)Zu2^ey%5pcR7KH18ori&Md3l@{cT;3W_G1s}H?lMb}B}kf7EYkYWP{iZbqR7C9_( zpkaheMU#wWXT2r2muMJ$j@90KibR_+i!FKMCDZe2en7EV=?$Ol2cMt$N(A(Wxd)cU zdq?s|(%w<6s{8D_Ivd+|#@jzrU}lw|)+49KhaJucZpN9#{vp!30A~*hbsb zQnJC(n#uIjN!v78EWAIQkOq_%v=z2JsV8&M3#*;MH0il!%Q_&P{ij{F^o=vdeFdA~ zyDM%bllxD$#f~pz{w4yyf!O92@a8{0XaGi2W#`{O0Xt0BgI*|qL2m5x)+xYxpk zju_db(NuVFT(8I){~m#Czmb0bEir8Wi~N6ywAft6I?md{@h3%~>PefZ_jZ0igo@!D zyJxLbuW9XHui0MZqD2dTKDS3e*f3b0$Q?l9V zb)uKOAZVZA4Hoo@H+zz7qJcb+jghzT1_4=q6HT)$AD<`#X?JH}tDo;r01La1bTlN< z*K+AHawB+(-z{nHc$dt!oP1=orD!%ne#oy3bZ`w9aHgt86SXD9NU3+Mx~5$&Q)Z7% zjhq>tkPcDLgX}Wgk4q7YP%;h!uo@}8yn|IKc2B4GuOAnCwtT*THcM;=-15$UNY$Tp zq#j}zOB~D{svad+NA%7Y+z2V(>sSF-i+@J_7={I;vRH^rItLs(*UYR^ z;xSR8^MTXDWhSgP5p`HI<=-@4iA+IRl6D@!Nn2WmHv%W2ONHb+G2b&w%uYjMDhj|t zqA+5$WD2%0fL7sm(dEAj{`Jcj{*yCXR6ZqK-27R?rJA7X_+`nvuhnx?{Wpv z&H2$qotdhXCEMzPK09XA^4DvQUoO;X>Sfuy-33rzdK@KIv&STHpU#WTMdK;|P~|Jd z4oaO3wXPp{Cn3k(jC8bdK$d|kNn_aB`!odRuGQN(!Uhx^8-R3Ch)u;0f7G3kE zG?$IkpSa?k!(R)RQ1__?r+A_ytcNpdxswmTb?|>CzpS^*RS@14UP~>^XwXVB^0xZ+ zw?DZLW27&PJ`LF>qvfRgK|&reTU7tOg|>*FtYS})paRu@6GQ3hIjLG6^K{z8-JLo9 z8c#3@FtsFkB{+(EHyJW!90EjHq)d!GMER-NiHOpLqwW>l&>O*Qci-fPyKk-VWf z(-Am3f4f;bp2=*D2cWmUpLN|-zo!nIl7dO$z)`qj@If+tNN_k!pPKotZziqd)a1ut zR1=$(^P${eJapi-sjFiw0pdB^CMN`f+E$JeX}OA~PE+ct?Gz`wbzgHIRCBmibkyw+ zdQk%S!IOPRXmaIIo{$#wuKGLv)`gwLD9_fDkT|b9&1n*5TRPvB!fAbmC3x8FnGw!S zA<~Vp=u(sI{c8F-US3(Z0dJo`wQsHhIftB=R>_fLTtM?)=|Kjg5ysUkk(Z6hFB#@f zoZR4!8k;afT2{wg4=bzSa^b?M<^Tm+^$-~whdMs9b5VVEmnb7 znL^GVq}<6nPC@8FDTL0>ZQ{8PY`VhRv_*Dw@n=%rcB3!AghMmauA-;kcIXZHY@I}O zGJBb@uEG{ha(Oj}i)A^{kn2D|C6fdD-7|Z*I@HUV?H2lbI%vZna*gOjE9uTg!h_pQ zr&mGu1W%&6e;hm@zo8z5JEc4%h8@2jzF0WwOj_44?R_x2)#hs}*$@Y68Oae0T{op6 zH)o@aC2{kE?*^v5%&mgJ~S?^|_Dacf0rvlP!lz6O^#eC>Ufev=~ z4HH<)7znGf_5ljcmN!`LLXw7jZw3(GI^Dm~dQ~(>b3YgulYT2H zBmVwNG3W?Ft!Y+R{BTV2*8C{WWHyTpW0(DYX}K>MBYViCG2=)y?6KtgLs%ni!?abD zl1$FC7QBY5XsfG2?j}_`z1#9d@K$bT@ba1MEibvKZCprln8oEL$e*Raj)v6raX6;@ zz+96rQZOR5f7<-)ikTi z;kpZm9h0kh-o4ZnAM}wf`y&;7n=_SB+`nCFf_1p(#*)rNQx1VOBqYUz{PML2;!(c5 ze!Hy?0*MqFOGj)A6(O3h@E;8~<3$|Q;JnOn8+>;3l*26+&?WE7-B;bGn->Lk{jHCJ zSCf}OfIU3@qqOQc8&U{%e)1ul2Qd3T1?X_rG3 z4$kc%n_+l{|aWJ`W+WK~kp^^Ri zZViBUj{`U9dS97-By`uMhR{`>RE-MuYich;#s$T$knxZz2GY7tK9*R#KTP z-S#h8EcaWFgYWMTrqH-fyTJDEO$h9#YNg4uuie*iKMGHdNUlOK&q-;-Q(nx?M<69i zK7JIY&dfjhdi1dGGTR=v!7#WJf5G*5MqbI z-rvi+oBP6UTUTaC9~mHQoTf$UNLZ3OL91R)!C-B1c5U|e#yR+-)4>n_BZD6@4F@7O z!l^DxUu^WNR7m`V^|f9bCT{qrquIU{X+H~LbF6)kyEQidvwxqTW#f8NSlE=eFP=N}!DkVgDQ#^mCHAzZE4Y@m8c5W-GbOo2TKf5+J%USgB zE$IP$a#>m$WUW@DMW7^#uKSj-@H^3kFTPf(r3TGPGf<~yX2fR_!%dh#vS76o;3Jv_w8n3 zeWJ^83z924rs6JMm+Q(`-C{HFL=nF{Y-gKFo$FRC5N}muRyz4$W0PkTbX^8!K? zTM0l9hX@!;Wr%#j9iCEMOMiQbxU;ac*ydnM-Z4i8Ro&^=+hdAjAOt&wzvMpIp+IwU zH33ih9Q(C61c+h04=iZ|`PdIND~~2L!-eA_0L*U#PO*@saKs;J=Z6AO!;_5bv-L5a zJAVJsUa}L|r>B1+c*3%vo*&E?f}qs_AqrBS`@TZdXi^>g{e?le$VsDm-*O$_TJjy^=WV2{E1uG zRO3nvvTX=`f~AC*wk|F@FuQEhLHvEQqKvS%RYBga&_dqnf~s2fSI?PyRy9x4 zMkpq;XZ{i6tWES{%SVf2t~D|zcR z=Cl1|>emrEa??;ZG{wzK48uL7EoNFu#$Pp~K`y z(2o%%Pv9vH2rZvl0Sj9bU9nNK~a-?c<+ivs$qD*7}+XkAB?g+a(=+OsDE6Prb;}VGA`k5FU zG}+ulFU~azZ4~i+c8soNr(nPLY#6Lfkdo$`J-V_dmw)oS8E17J%Dc%jXh5q=PqW-- z&3&FSCcD`ESIrQQy2X5ZurL2@_2UodYRXTqV1~Vb zhjmPwN5Ac5tz8si4_;omW{X*ju;E$9>m+QUa!HStJJ;07l-qjH2%LE{-I#(t>>f_< z88*7aIqV@Qz8Bz;n-O?baEk*gn}rbz>y3)TM9Qh){<7!+K4HS`+r1|hg2AgOsS_IP z5?y{d&H2g0Th_Rkz~ zY*GNeOHu-})~ZUZ@S`X4Llo>mAsnmb8JR-cQ6*>r3+`&pxmKhS>U)s;pUx27JvMTx zvufXu)zpt3pB4YQPI9o_!HJrT_0yot+EnhI8pAuUt%pvf&y(4%$ZkUdjHzpm)^5%8 z>g2NQjq&feu@;7ZOaI<-_)TCby|vUHf0{Wy99iO&OkdMk?CiNJwNFFt2d*uX9Bclq zB}H*eAw~~YsTXNep+Jr@*|e~G#$nmU6vfTw!m7|9zv>5nA{fvh>0na1rSs_VJ2adA zTy@LM3y*GHDullg1C27ijrZpJRQK%6VLk7ovc=7j)dpU}Bk3haL}M3)_m|}6bjmVb zLy04a;%+~g-9BxY73fH|`&atstKdJqINbT`HU08eVd)m9&n+E3OOT^6l3t8ckR(SE^1emlfsn!`3=B8z!jLGx<&nLEhB zrhQpmul&WSwngg1t?3D&n4^h@uSFYqcajaxKrihDn0XJ~qfc|`l)(51NV?KWtC`2V zKI|Qj%cA$1Vfj(H1DEr8kICru4DZpU?QrH1p1B#HZ54xo#j^Sl*zzHQc&U(;TuA44 z6Og$gg>KrIoXQ%TB|&ZX$0ZK?K>UL4gpf(OV~CKkPqNqTO~2*Z8KK1yURo_V$x$nK z|ARoENV)sCUB2C_x0)wXrUA&i?KIUl;2FOYbmO!gi$wMfEoIdDth`v`>Ahd`^f19G4xU}scWQlS;#}|ov`SpUc~LVS zhKe4KV4)zo^z{o^rfpEVfuHc%Cf_BD5EBBwKIbj1QuOyO3y@IDc$*^F2TJIzUe51b>Ev2hmci_Ckr{#u;Ph7GQTeKVb(K6)5!(chXMIqnN< z#M>Y?=YsHiIt@6URjU3KU}8ij2y@kGX<4|SM?sAqc8l53AKk*LnXImENiB8_J%{A2 z$7B0Sx}wwi4#0)V(gW$f_M|?p6)AY>PUFP(t6tT@9`;(1- zPKx}b6CK0i$yCv-DzuxIpdzYu!)yDlp**%%UCN4~V%o2p-0s*58tK}6`<~o@a0wF9 z&^F6x$#CL#p&)xjnm4`Du0Rj#7K&o>9&?yF2}=g8{2XR4Pb_99JGONjY-Vq#lr{z` z@3pN{K--XCmcu&ll00c$pL}3TAo`-4=T%?$1uYh=$D(0Wg*Q2iZWa$IfrIOfU-1+g zI-&gzv3mlYy9oUSL*v4&BQQ)@=XXjoKX0o!{J)9fky1R&>N@t3fS1tI!9}5=;lkia$l@O zvG|9Q=79-D838ZW*QqkjE zgmGrP6G0bo!fKDZZGgep3GxT0p_%&@N7#uimxZ@3s-(MRcTLFZog|pk1O9EK(oION5~8&^*Uk9%0wZb5C={pIPi&8KakdHZ*26GlWPF<6I z3X75*Zu-0aF^8zMmvHFGX7a?M7_x}`T_?o_D^andqL%J&a=lk}5WGxTcvjHs{>g`g zy%V;WjY$&yrG~4zN4v7xyKOYx2D7?S=^SD2oW;VBfYt)4l@MLg@bU+k>O5v2=ZEF1 zwcqLNuz9s8T#;o(h{CvUl*?5bUyUctd2l~)2zpX(z2xLIhAq3}p1W9?<-}V$ub7>l zJ9reQvbk)E6t)SNyf0_y&bfE1c;q7%_IO{lw-jx?w1iUzUo4z%6mmptx?K^5uaUV8 z#;0{YIW0<0EGc>+3e=7sm?{s~29LaG4Ec?&XwD+#m!#Wpui6jiS|lm>7^FoT1NM0J z93mNpk$j>Ql&5M}2yKhLxee}%rIw~K1vH^F@v?tl+{p@s8Pg$x>97Sc-llowvNH|mO9svGJ%`QC6RMP61 z)fKgaJS%PHJi2eCZIKUkC6pPz{ames2hsOcZp*q&mF%c;ZB^iRD3v@KY+@%pydmYE zHG1&IYdrkx5 zz9Kt787D2O0PAo6+4$URGj=qa42{-n9AMrf*%}%LpuUP1+Um4X-_-6jEYIPx?)*r+ z$}8jBY@9g%c+I{>F64t9u`G`C)%Ch(Ju6+V8n!sXpL%e!Ith z>^~lkHWrMzd8v7_7&!i9Rrh$9+~f*{>RMyF^*rs}7tuly%Y%s*hwagN4Z_CybnOS7 zQlqo%K9ES{-QHgiM@7OT9tz6i<0lZiuh5ryXUtVNG3eP>|T z%UY~-V`_(;KRBV#N8bzWOwYYG?Fyl2tfE`LDd<(kgJ*@s|3(ylws7u6=8tf8i#wdmCR6I$xP zjkTf0R&@&dvl2%)nbiy)zJrmZ@S%zPH$5kpZ}|pNSB&vb8xe%*c>%NAx|uPgoB&YY zKq(ISDux^LXnCR+;411JDJlzxuV*`JE}p3R`9W(zJFP3a2j=x)Ft{whr8dKK{l#oY zMFT#vdsyi*0j{aBH7i2(c+t&W4aHR;%}Ts1z*hYbw|v!vyfJMP)CEt!=32LL7~5i+ zy5VRw1i}Y6S>-mjgfQgV7u^ZcnA;$B4^F``uZ`QbAAKSF`?hN}oyMv5kffi;$Yy9s3xOZuDj z-O91e>fX4q+4slT0#L*2WwAStd*pHIi63Eub*1(M*f({SdRJsfWGJKDm`ic2al2fj z=xm|Vdz^i;CC_(k%2;e~BnSCd;lAzR-HuNl2YNrI2JS^-64Pm}f&c?f%j9&2 zZ5hoEblEM;d;u30P{22PKP%|I3y34z32)(6OwB0ut(?N^a_)?S)~jVKL7O8;j`a1A z@Ta}s(3}R39XA#-i8pqKm4in-T)V}KmI?bF2LejJaj$(S{Qp)jT26NHJiCLP)@)Um z8)M$6w(049^vM&x$4C(zZq!ZK+%JO}dED{gq$h141;;feYfdT&uFRN?{YgP8BCQ~(q8d~MZHzyTQ98frq1$N zrz~R~CCgBLl7|ns&ko`K*%mV+b5Nu0uQ$ndcEJ|};38vo$a$O8jGzhJ^lCUOaCr6-U1qlf(eXMW@E4b6JV^ATB-3aYAsgh}&^NTT-n4&L`a4NC8^g~g@z#mmf!TX+I0nWtr!t=M7 z=G(-_fc`&s5--!*VDCixKeLxJ@1VQ4)2jW!aSmCCQpkBS0@~7G5!lM6z@7G}f+;3d zcSd@zhMf%A9(m6Bm2+m~lbV0sG>+80;;kuYv>f9{%LE(g-ZEf97*nOzQQ%mv6)rAy5kAo zxrZ+fCzas;wwgwDvCmEyu*`Bh?r7nu{)5$7u2OS5_D+cu@Cu8^h|k1tR*8wsqpkaB zKF#<}s}VeohsgA0_dX&{MLHXTL(;~G*H4zXSR0>@b)UJ)0f$Xbzl(4 zJO$LD+IU3vn=Ukijp>&Hg7OOgp1tD1cgFcaWn#Vy=gWmq}w~>L! z`Q`>_t@Gy;G8Lx$@|UMbxE~5VLBl+FdRjL6HjGnmFj;>fh_(CerBjLJhucIo!8Fke{FLH_1~jr1O9=&yIe0NICC~*1ec)xI6$>D8tfqDf&$}CT81J#yU27EtY5Jy$z&| z1NIK1r_Byh{Pmu9_oX);8-0#a-t%@7jl3X`APIZOo_)}eT7E!ugoKcse~Gd_Ud}ul z`DSN1VfCf^ZQai-#0hGrfH+k5h*VhYtMx~Wi?~~>a~!3q_3tg2B>~j4V>+)$Oocl; zS~#Q%i3@^WNc27V4l1A30m=G7UF8nODSllcj777Z4{c0S{@e&IU7dGivvRZh@S#bs=z^m7Jc4xur|CAqiUD#j05v^ery1p zHhY`ygJ}R zr~JK@?k8tn%Du|1xgFOp6c{nMD%88wXjU%`2nTkJ9<122I6C3F`*Oz*4v;lloK8z` zXKt34dOWeRRbuXD-D|{Lw#doByJag;JWeU z%=To=*=4H2ilH`cEi0o5R!E7*;fBlMp{eQA!MWX54iy7BRjg2KfD!Xssbw}m$FJ|K z!fh?cJ)vp{&}me-ZK}_X2U09?Ha2_nF|>{Og?)%l>InDM?Z`% zd;`0T6te7_f^&h7s^DfkT^o@@X5+!d@mia~{TJl3Z~jLg-x<~9*8FRE6_s`b zr9(uihZ+Q=1_cEXNkEj|6qOPJB=kVwa1aAxXrT!KMEao#gbo1(5_*?jloCoPLIQz= za(T~McinscAMUrkX4cG_z1K7I{PxW3sWsm;C>hI!fM5k+C)WaYr~R-+wM~$r?5G-0 z;%ECHe1rLi6hKXH>u9bv*SAU|aB;2i^^!1y2cXn%Ne1V|Kt*jV8VpQQ3;++398`?R z7>XRsPSm`);fVwdLE2&yi>qI_s5g^Jz4rNC`qo^jg5dzSyh^M$qX*(3Bsnj2)^S3p zeA3;&A+yZFOO`);oj$WnC{~u^%IZwaDoKo)FkKpC0a&uw#lN?s~ZmpoG)&; zJveVx^`vYS8P-*6W7{DWxWR~C%FVHhUzjr3ZjtkBG7-Pyc{RF$zqhvh9O7U%%gPI!j!S_~y&ud^fxoMDBsTD;~~=B`FoVHPGZd zC4E(XJlosmo(WQGP{lNIoYk6S=7&(Jv<%|>aa`RZ4)GCf?EZ*E>u zhnWZn{v%QLZ(R(g2JdtR@f2W#IXmyc+T!I3@KL?XHM5lEnBszN0jVc4Z^*|N^Vm?c zZnN|WTcCyTYFgLNBF--UPup1COIz~|r1-mcWA97{uOL4iLP*XB8RasgKclx>vrVG$ zE$+z%Byb$>&csM{H?V4k?sb>5+Djm1=TWxm@~(%d6jv-Wb1L-{!j!K1D_Hb)fL+zb zb*rJ0XZj%pX0J|C@2GVwtM}*cz>jaGrlu(Lf=qWPzJK}-!k%ZVBFa7-FWlULMq_ypjY+)yK zOZCtk4;+*Ey6)e1A#k3L9uREY&Tvm1*Qo;<^|A!DOKu0bu`7(;4xSq+zM}g(8~KJ# z-pbd}KY@>akBon0n2JV1esPBxEtmEzRct5mo}7!4N%-Ouf4yEWujR^Ol(i_^H9lL5 zd9>y0X9MTd>$L(#Z53Eqg!zv_Usya}G<$?TApd+vs&3wT{if@8&Mw47Niw{3v^3;l z%!c~;*~|D)sUg3L!+b7 z!WJ{=*g49nr{Rf{u|=VF2^hVo_#m5{Zx`%qfTg&|@=b0RP3o2+yIcysDCN)^%|iklyQrHrYc?ebcCgHeQ$4g^Y5PA^x~PZ#_b|w zl)#FIOW)EFl-OM_=>vB#o|aWOd!#E8no*;~AGnf%c$MCZz76hp4;kPI%mDur=M7N@ zYXA^&!OxH1_j*fi(h`TSSx9ko^L+RgWlg;Itg!ydn2x4l{#ijICUW%vWYP7;*$4Z! z(Z7@BwDw7|e2D&7-9mZq&49aBKhLMSu2J+19GfCo)!zpy1IievSPWJ$=;ne>ltxdE8vfqs3t*4x*X(ruH*5 zeTh|ti*bR5x?*YTqh*_1nx2Zn@!~GjkHkZ%V1qCAL#7H=WDvg5`C9bZ8LoTu4w*ID z{)#KcTW#u=rOlej*Qp>cd|qiqzdS9=+jzS@?3W{1q$614e*_PvaY$+7XQ zB)QRtQOD9o^YJndep+`W-yEj4%j|;t60RJ41N1wr$c=jS-8K6UoPd~C4=hkK^sZAf zN+wXfez#p-s426)HQ(gOhQ9QsUgmRh{ROx2v`dKRZ;&ngJ!9R=TkqJ|nFFDS#B`AB zfS2b5%-=&i>9pl)TC+jC=M8TcP>|!}fNw&a&LZU(YWztQ1h|%UvAkY}__tY=2AyxO zbJgj22rqY|WaQ-WwO~Z8_tLj_hu-yhgDSjpJ)f^yb7uuW|+5@e6^Bwow;%L;&{3BufSg|Nca4YUnt*yd< z`{xdY0X~gKm$Rls6)#ik%_?lO$@Ht$!mmg1Hp_x&a#?&GwhDj>MVfT_uBsC=3fCNR z$2v#kGdnIVxROOyK#EdF)rFqNs<&T#cAksO#su!Y{Cp?I?_*aU^XoLHBb4QG-@2gY zSjcJ)Tw6nb{o-Fi*#MNwIP&0Lu1Wf4$XL$oQX|>19T~FE%_Fru2{|LD z2Hvk{zEX2bgK~3u{|s`=T_+4F^-?2!j!I4wM?isK`8sf@=RYk?zx!z$yh+7b>9PfE zKWg_7fd@%>!;~xf0#Ew+C5piIp|A37)z&kmr0zC&E^7|g8WoAbv7GYTgocL_4oXV9 z+9F)^7Urc-TOoyU?U!T^pSaB@b<7mD3{UOa!2ucbo2&hPE1`^|i==lE4Yi|t&w z(m|QIU$}>jIJUgbM8vlA%8+*G7g=E&`PfWf*Cl4av2%Ii8t?mva2cIkw;59<*=J1- zatiKgsq~D3dpt9@c|ZOLx`Xpv$T3=O7mjV96gzRM$H3aGdwlXWpvUJ{t4Fz8AA1 zk~!+v;6hQlJPO+^eBZp@1Nb_$G_UO`=ToRDXIin|KotlnPYtODu?e0s`A_M zHT>b0u~I!%`no*-erl6W(QQGa&NUH$&f#ZUjc(BT%Ex7;lPzhbqXg`CY+wNyxj5(C z%g?9!Vip%LaAo^?y`Xk*o%6rpcr@fAXTwI&>~bGETU(JQKs0&y`?ze-^L(9?gcnBh zfH|3!P)y5iNz(ia?LYmsm!;RAG*t(DALmMjF7nlgG$VTx9`k17Z&(P zLwWcX$2_!Ie>obAR4{y6<#VF=$1EC8Go2p)p7&l7 z<E5&w2_!-!@1PR_$9( z=j20}>Y|D8b<~1Hbnu7Yr2ia5SC~+bAI*o3L6v5;Q_6~q;)S;sYP?z|WMH!z2PYii z;k{5z=<8W>DcJ7MH>J~$C%=`3J8+SIr9G=4p%n6K^xHeuZu_=4`n0S=`A_}^ZYn6AUO4`D@ zb5!-`hd0yhtKFHM*KeoDL-Jr#Coje)dgFw7K8lCWlz>Mo`;7;G9VHsy|CBRfCzGzz zPVkVBn=N0q7MXaKhLU#f-~X$4P7JWgp;Fqd(ZIw6f!3k_m{5Fnj1gF7wS8%j!q!3E zSa!PMq$v8ak%hraZI>we$+`HTA@#!J5p-qv`Fc8C6kRuQ*JMG;o9(3n*b~ltbrdlp zc{S>>U7@&_a^BSl>nVznP#)&azdW2@R(ob)wuzDTId@-5O6T|qkf99#@%;Mom(xhc z)jbBg?PC@Z$Idp8_)3(ZUU~X*7>1B&s4>wQM({w&V?@i7e(xQcq_n?Wxs-gAX5sCk z%>W7demt)>>oVS;j3T}jN2vo2e6#%IN@rujerV8OGv>~uC6}jKV=3O0)W|UWS!NdM z!e$$tydlHz+YGUu0wp6Ua4X#=GI!q6aW3BBC3vncdS5c; znO;;lc;Jz?4^1ZbB1=U>xvzCBNMLh`v${UOCDxHL-nj(*zFp|M3M3wH{{4LTfe1(9wSXgIRmJv+$O4^;I9Ix%d-fOs z71Wnvd@T_-;s3>NZnMSfRQvux@!g+%La3jf+Nq5}0zngdYJ7FP6oY1nH3!8=6QzES6gNh4uHxHe9F*t~+v7=3 z9D_BANqn{-S>IMMm7jxr+>9v;`b(+`W@qOxxRClwC5t#&{XKt?IKh|#D!NCI#|Rrx zz$}X-FgeRfxVXbD6t?)^1i^J7BuBpy=t=)0P3k|S4YT2t;dOP7NWASBuOHsgxN)3( zO@*#qWuw5#sOB=Az!ogqfA4b4&bKnsx7OxC&(LYf@}g6R z1n3V$aI6SYiq|2u88=g$^4Y7=xjnMgZ+2ty!OQ*m7cX{4dq0JFOzkd*e&yLiO_OSH(O}A6v=a(;(csed6 z%opTflsLEx3g(^V(v=ALOS{V*yVDi>hLx_i*#z>?v2Ms%8OnElK~erS{ZLt-?%Fq! zI`NYl7+YRJar?8Sj`L^!0)6M8EDGX`T2zE1DOqY|b~5+uc~5SA?#(FdtLr_Y3q|#J z5hbD&?mjX@9Db=e6594;E>SHDf#U?qu!-Fnk$grh_%A%M*7W!3T8PTMat>9K!0C`w zaZhVKggGl=g2Er(QQvgP?@W@c5f|Ggme*I_8m~b1w|#g)`f^-iH!Umao;H53=7f() zmTHHs?CiLC>_?3p)p%CW!k&hU>l^hI?e%KbM_qi6;VZop!}kQiB+?`SKyxkw?H$9J zR=372EV}9Eh~I3jmXMwj$2yHqL$$}CIawf_;GqhdTGcabPM*qVwmN>)KXHuXuku8F zE&u@I;L!0@B27Jo>{$pVIgjVmyX3qBc$O4x#eQlj!;TfzhgCT-thI?QP3FBUd$cOQ zbx}D<-?-#hr-{GGCt3JIGv}TuyXxWP4d5=sxq5)nY9E;nC-1xs4p*OHT_jmLskCg` z$P(RMzqY$v4ec-A8~w(iE#SkP1|7aZp}yAWDrAv!AmnuQl?i+9n84Y;=r8xCh8?4Z z5-PkndW|&?ffDkYpr|ukjvu&M^7;>)B}>~azDN-pJKGD32ejbT>AGMihDk!Y)8j1g zv>XwQ=IATCXM(g>Mh{p}l*javUR*bD5a8q99OPcC8j7t`V>^_Rv!~uz$F@oS>f!6u z6C$qWT-OtPOWF3z6bz*)i1Nd{X(bV|^8LK**NT%EoEtDWr~ zj%WAgeI{kKtL6 za(Gn;X?rA#l4e~iwHwG|E{!~6G8kj?=<^PuLW*@90xJ}5mZJ1b3knMLN<{|_5{AJO zKwp{r3Hu}7HnM>wnI}_V5!lFKc&3&>F>@*;$7?= zgL6w4v1#tp*-XSceI!zd6=RqF>W%)_5=0ypnT02ueOQ6e0g=N+2M{1{>VX3zLCCxH z{3JBPM|;D!Vm#5w>!7UazXH^RwiO6l-kxzY1m{%OcK7K~=;#4&4v13LbDFRX zW^iFR=gulcKeSYDzMvpxm@xr_$2AHjLF@tc(-Y%bzeQumm<-yAR=Z`h zF!%T${Jhv7l!`4%eK2zU)+DW=u=X$;5WJv>7vEV^@4A?=Kct1UUl$$1$8w(CZHAoo zzv($dhhvPVBMUG}hMa6GVqov@WqYktKw#X`@L8Zw?L$lg;b(g;VNUfEVj?u29Gpcs zAxY1Y4(6RIo+spJF6-HGmot^A{_EKzs{7f_;ju)sZS4QCsVlXn{uFy17CI@ZX-5d% z{!l&8@Vw2{n*Mg{^deg&v$hJt1sPSUKS;kBJhJI|^=E^WPTw9J+?|gLa`58$uw9i} zW%AT@GvbxPYltR9&nv3~#~u|6XjL<1+IVKsp+6=lZN8?e@0~+uB&EjY{0rPPXIdbQ zj(TC{WbiPD_o;P}PMLr=EUg>O46g(>o&7TZ(Zz%|i(N{%tQQ(x?er7Rfl?L|E4vQi zN9SbX_?Q9MfK>|htO6mO?PNq@4HlJEkNP2~PkI}eurQ2Ug^IxLpqC{izz|I_^%4%J zvIwe83Aq5Hk)`9)p6!`FKpq(yn4($I!^WLvB`pM8kJa8kjPBuuw|?~$cX!9bOT=M` zp!Bh9&JvHNOluOT!cXqZH=KqD?L$Q1Ytf8Y^vPG){8{#3UAK>92A7N_A*#G#FW_OgRr?muJ@>Xf9_O?zp6HQBoIZ z0@T{U(=W^4c*`LxjehN>3`JL8<8SeP@|d=!Bi6cr(9ut>0Xmy1Ps5GET?5`>=u%r) z;z~aRwTf7%$PCVB7i@ z6u`WIBso$=7vdo@H9zDw39nCJUFA;GPjwL3LoFzG7iN7C= zIPFX8J-jjU2f&g4UVRCEXDEixhx#kBLRIh-4G7^g`Sq}mm-G01CC*R-F_c9mcHO!{ z{eJjSgD-*iB4R4!oU(#|5jF!o({633^MpZBu>r*V%3%s&Wjs8l7RA&>CuMo}+F#+` z6|ie{pt$;OBAlr`P0udmr1wh#;anm3`0x`xQlGIqPRyB$7Zuf^b`@fyw{N%+H6@A` z`hLjzMA-3x*PZh?Jq51kqu8;SB2OuEruZI~Y_uC)CB7p*xo{B7P}XYw3=fBjFwER+ z%xKsEth=8FU~GLujUC+)Jhi9`JgweQzNr%^rK14P!?Y1Gz|#xzbAXXILuADa3#~t+ z6uNi@QaugZF0xtA99PsqcByH(hFGdtDAZ`dsM0Dk7cL;;ICwIJ;<70ezhsHOdDo=( zKJa1(+l~Ld*kH2VV68@FEiSPAU(|ouIZAOBg(@HNR-gY5nHzTpm-wZ4+>A4aZ@>J1 rw4HI3|K(sww~hWM=l>^j5Q>-1v9h|RS9><5@0{Ty@WV1)$G`s%8T0(Lq9Q4m58NJ~IP=@2Orq$DCuq)QDY zNCzQAYUlw1#1Lu%gg}5l-}k-$d+(fg&)L0ucV~8XX6~Jxo!uMx&_tj2q{vAQ4h~*J zgS(G8IF8mFmc5U29p*K%-v&82c+NTN>OM5o)xG%8+r!b>^%Vz)LF8vMZu2Ky7tmIZ zkDrh`edJoEU$y~i>evygorL726H<5XJS@wKK3RP1{Q1&MnTu7G96v`&WiF5Ga-Ki` z%0_nNVL;;1THICx0|BEDzQB6fPAk|%VUDL_=2-0LcM%RyGyIY3YYt_Q)Mvv*E}=i1 zzkhM0l#a|=AEcxhaJ;HHsH<;0aoRZqZz4j2bQ~0Xht<4L>Rxo34brpK17 zAAvbOsD4-b5|5E-+dB=7x_Fx-qUIc9&fF?$V1DGeqX1C!co~r6<@IQ(*&|;s&*Nq7 z70kU%bMggH71Q=^8{Aq20u5YxE~V@GWnULCMS78oY<*egFJX?3e$aXQ zsF%Acboc-ZII!~mDb)fKYSEjIZhnyiYVj?=r;1-{dm3T&f%>TLH1rnS_E-F<(Yg&z zQuekzspA57vkE5<$HQOt$L`%Kib?Ic}=W)9+(CkdTVvwqls$k)<9t8{#c!k-PfSsM|wC4l3KP*k> zY@azPaBf1;+~!ExNz&}ZlF&!(sxN6`A~Fys*T$NQ?juYWu&g3W!wz}Fg6wb()(aKs zW%<*{?PHyK*smOmjd8E%O*KDV5|#TD@bX>WY07kM4gbnV{pLKNt4WRKiqcZYQ!(^s zp$9yd!_MeNJeSZK`Q)qWdimnETJ7->nEji_^35{ zTWRW*t7rFG(@$J^D=#E>H{NCkIXbiOsq&Qg+aTTjPnx~IeBX(IFF4L3cbg}yzi#P= zC7f|55$P0Gc40`5ex_O3bFW9G2EYL2cM{Ka^iQzuW7SKM+oNZ#qQqKz?WN03nV%Xy zo-*Q9cmmoIcYobLN-O?Z^O|ef4R;I0IJ=Sd%&AP`Ot&el{2arGXp~OK_@b~UbTp6B zd-jAs8=k8(5sU5dyfi+QOr4wl%k|dB1LYy+A^cNY)ZUt1+^u=!t^SFpbR5n63I`zI zB*)&KUq+Xx`APwY`dCwP#K}1g>KmTpZ;w8mG0T+LBXJ}v0;Q)<*1rWF=OW*}8Nq$c z?z*)Xxbt1Q(_4_;rK?;ycIVBxum8GGDmM6Ddz$0*KXTKO>Zj!2@p*Dig!=Yz+P)87 z6X&^n{LY=%$Hn??^PEv`zVEqx;Zlx)@Z)!`x{IGk z@e-Yeb4U9lCvMlq+s^UQh1(up{}H*EweVMHP;66Q;*Y=2$=|w+KX-hTYL8C7eV$d< z$F(6Dc%tFmn@okiBRqD$g*cU)mlXYEPe->uF5`Ld0pD`J)Vok*JX)oN3FoCX@06OV zpUERXnBW_B6hgl(U)O?*;{YYUpzB=ho58%R?Yrw9|A^d1+Uc%Ieh^4Fq1DWDM=3MC zG|@A~lPg$F`vddt<@nnb7i*hVZ+uKKOgFYRnYcTluXTG#5`2a4lYyjI0&iE^IpY`c zbi=mn*zCz{a&|$sc)$M78>63=?($vAOZYf<*&7rf$P!{*X9)*9G&Jsd(Dme+Wn|V( zBj=ocz8uFKnfu!JL(lm}9cxQFYuc49o9+5_DJMO1BCr10^XID1w;rOu%RaRIT7NsB zksdJbK5zPz_lJtgCyg)HGmF0tKQw)~`jFKB`X($DDEB$p)arIYR>n8&Y{b*df{}s` z1)fhA3ZDMB`Q7?iK+cbBm)yk?jc1h)E3G%bE}kC(t4OK9DsQPIk7-rjuat199^@LN zF2*hPErJ%ihy3y(4?~J?_bU>Q4^O|A9Udw6x=&UT%S!vhNp;=1yu-I69EiN~XyA^r zwepklr!b^ronvEi*syug-J&m+pDpz(YlVbb($DmEIu^bJ*iF2Ii|6S44msyH`14Va zzKz)C?VyS(jH+4P-5|^s?=nH-L55w1Y{rmAtOmc2mko3>vR2tgjfU=97A*peL2m~6 zbp7SbT3yTx91biCEZ?l6RZ@BXivA5>q%3+2>g14|ekragciuFIdiH!Jescfoa8=P7 z>OZk|>`640(BtTChj!c&r3!x(HjJqft`y}Ju@aIN5)@ul4z|0aGyDX ztW&wd1upLQ?1YTF;X!dCab&S*K$;jy-9oc0<9bH1=0Lh`Iz>|r z)F=*BPS$+G7#WxML7k8{vbMCr)z)I8teLf2wO?v;u+OlQ-b5dy7i1-PDs=kHv=S++ zjhvWQ;Dn9p zj?H^l%dgr$b+{S9hqM*gyV!pDioyaivNcjkKt>uI>l>3H5F{Tbt|sShHnF??cKh=> z^Q^3Z`AQ)T(P{8&_tfrv>QB0V-}u4g7Y)U0{{6Lv+KDz- z1Wj?Ewimy9t_kKYn&8oaKBMX`wCr#Zw2}XJ`2>*{M3NnOCWbx zbW`U8Vcj)^8}-V+Z=f0V0Z{)2I3yUJMZw`dQSyw(4YmxzV5JAlJ>ua6JK| z?XO7p)ujZD#O`++HOe|EG;SA=OGJakEaS_*A#!NH@dXiw_QJh*%f`kk*Y>&jmOuW3 z3NQ2v)MyxIrRFyIkzO`$HT-1QXxPs!V&YFlpAY#*Eadw3d9~}D*O||#&*TbrN+Zju zi;j)>pz6OK*Cy!!1$MM$x6E;oANYdqYJqA&*N|EXuMR9}BBWqEzpmVeLKLw#Hgj`b zrfO8z*4hNtJs-~6OxT&4O!V_xIx#j-L3CfOLc}0)cAJ8`_5~aB_JSF=THg(au5dHO zZ)iUCO$n~aKH{7*^oP{xvE*{!c~R@ez5%q{j4`9Lbnx9-$t7Rwhp*4P((kgij<7gE zaiDs9(6?2|)j9I8a}m=-3R4>#I7{79KI7Dj;ROXPrXOI*lm+_&AfcB5jH z$e8sQALI=dUNc$QFcUTYIhPL^x6tdA(NVl(s~sTscWjh4LsXcIsO<|-o>^G7O7!rZ zT$l(Bmfr29)gANWUX`mct%1;QE#GG8HiQoyl>&gaGD#y|T4$t1$d*^{?ofgC4JaYTG@ zO2U_Zx&7c{!^U5+RqlsQl=$4!USZ{lS{Kew*&PRlhfOVV-g4oZE_nU=wdcT}KSZ%l zOJW=*b6ut7zSG){9dGX>!E$^3(8jtVl{TKS-llJA; zK0clr3JQLHe)4|G@*dt!3Q8alNI~(2!i^hphY)hG0CyjIe>r!U#D6;Z|NY#31#|Fr z_VjV~aKHGke(hg)`1)vHzWgsj|9$;uoUiEecBViVFYT_aUm* zziKreI{UwJHNWfZ_R1Z0NCT`2Qq}r5{QqhBABz7A)8ctz9tjGjo53bg&zYuH3RD*dEr#LFono@Oc?Xyt#;x zOlif@|C6oRG#u@abMtI0K$0Fa{!jBEiv6)Gsx^d=eFo=CuLZ9TF%%bnc-~pre`qhG z@vyk>K;GW0rZc-5B2_#6DQ~RBBln$(Kr-^h#21Wo%4<(`QsGWtaZm*-7f+t~!So#Z zqDpdJ*MEXR=Gxw;ZM|Muomq10h!b0;%T8jPNM=dB(k$^_@^f0|oEb#Kh)L;R+}L#~ zK~C|yYn>^LPx2Y}os~E8Vq%NyQ^oe;VwFZk2222y*8-00Gc}_h zSF)~5MOe2=syKr5ngh_@rRN=bnn90e!<}h-G5iaA>(C^-{abeng_!uBCxID~HuCRNpCtYWOqL zpy@G3jlNenK6g)8>1P1Gweq3-`g4$;&V4n{om@%P8#}K$goIvRTLBY|;H~j%8mjFt zp{8B7nASw7b)P9;f`7%jg5H+bnZ&`X__C)1jHJ-z z5es#vrik*bos^Mj-1IR8A(L-|=XlR%9bq9Zi!<*vt!mBHA^{zBD%d)UQ%c?0vFzh5;btgb~`xa^OttN`3D;AQe;`q|iqgtAPa0 z#i$B(iRibg>l^w3vAEzv;@?HxysaMc$7#(>U(S=vH{}VsfR0$V#yHF+8_R1aoVvi{ z#e{J}YITs$+lT~Th&}|?@l`^(EmVa}nI9l;&GMB|q)J(MYhoVZcDy?&|5@Nu`p9Eh)woUQzq zTXVMQv8fj`Gf_JKQX^%9bq-cYspaF|7D|M0>ilHH?X)LnyPu#bl}<|v$0gb}s}IAB zg4NGO*TUxn(ymA#U1Kf9mAcw`KEii=(o(l2RRA)g>5>vJ)+EHD`Kee>fkYojWH`Jd zKs@**geTZ5*=OEmaCXWld?|R=55o*4L4TY5+^un(`JhHm@|LLj&?K1SEnI27(e3CT zf(Z8Xj3+s1Vo1Nb?nQ|f5+4CX3nf)-enXmv*9K$y1Mag|7}oQj!`4LXn(QRRB!ChN z`daEQAt9dcHZ``jfS7Dz8z!vIVyB7M~T)X}_rV{v+{S;-HUWQgjz@&nf5|bQC za3e;dUbc1(N5Eo(YdWQ;P)2+cBb5_pQSd=YogFQ4U;75Imm!zei|wp*sTEMKOrxv% zcm6P~*%A530C}+INZh3>kwBn!SDc2BAwg!7XV_um&i>8dS%di|%H}$mQ=*Z7QR2Kr zhgWfs8@m_CXwnSRDm_CEE;V>DGFKZbFYASBwb3YXnDJaHtfhRDnDMO34gGvhFI3#h zVXX!6SH4Mu8;q*-FVb1-&8!7?9@8{R2TQ-T+FacE|271&hG{81JG-fNA?pt`GDWG0j@Ju8Y2!y-Wnbb>4m{f7mjI?%UqSkTN-qY=2nhSbq zS}q9+R2+B`q+K%1sq!(lh1QzlN=f1s3?B8YF*WD%az#Zwg8)Gz;)eSVQgE#%x8wiB z2;5(l`8(oV||FtPY0F|y%{UI6$9(OqWq!D|k6Jux+U?y=Jx~~d4c*~teK1(|TiOFui z19A|D-EOya37)xM_P0eZ5WPF0kZ~y{ti-?T$hb(!5U0N#ts7MoDMK-o~3;f90 zg;l=?I9LEgA)3R znGV*mK9e;j63hc2d+pqwe11u1$W=6@l9$Yx$V=KJM6sr$($39d z%lZY#1AhGkf!(X4vq-@miTr%)p}vUvLH(+);Vk_^v1NxLK3_?d*)iGNVwgBDIPfh> zbi04=%V&@`_CS+lctGCY9YC{KKK`Ea01#BCg*nfC+7vYNVPi>J>r#dfd)0bHIczMg8&Ke+^J-??f@7Wc&OvZa{vqV4cX-Se*HZkehJiU3^v_lXuSI8ir%INkR7Xe%EfvtTn1lEhYv&+V&w~9OFlGxG8#r&uMxA)Zzl;kaY{V1w@u3+e>RWyn{ zchN`)UT*hG{&~Ad-y2&bc14KzF)~z5NW4X`j2~r_dJ3E!=!Lk{5P+s30Ohkg>Mmt% zo+xj}%{FjSUM6`3pb~No18ZkUth^o{ywePHV@pxZi63L%;3@Sv0Av;+inCEj7-}aW z_#0$o+gfAOKbaStJGuezLR&?}S3k5M|6mFJUiDX(Qt>yw_aMgWU(*8-n8Fe<8EAyB zHc-)_+<8rL&g{8Y-Ted|R#yw`E$q%cBTAc++m%l+bgd}xu@X^C)0nA6+@dflCB- zf7o}uD;ZQ1+PO8W&=qlQ2$^!0A187_huz$PZFwijJ)YAs{ej@{d)+?UJEJiFPWQ32x1E5WJnLnLhMm(4m*}fGA9C_@y&)rJKO$9iAm{<8Eo9M z35U2mlh#Dt`j;)CK77Q;I@z`?g|SMwXJgZq_RfCig}Jj2L*zL(BdmRRI&fP%DH)QR|{FdZe-_+D=FaBMmt+U=^2-p50$W z?g=fVWJBi66-Bq>l~&%gb`Gy?MhOv+fxOYceRBzV=M^VQ9+SYhfJ&miSEy)Ly?U#G zM?WKiED*Oa%_*>)5?TZ&crpYze1-p82lyDkbDnj5TK#`_5{NQ>D6`BGV0?$HJ5>U*Y z-WmA@&mR}K_73IqAf`jhb-qn}e&8*?&DkkSo{%KZPYPC9A6iI9>HIq_$shDtF$pF7 zMsD=@6@8|LXJ8MMxD)HIZiJPJGdELB|C``7SOee=vpolM@2Fi7TF{-q&Pj;xDRDU& zo}@Lr_li5q1*{PXpA{)1KF;Xud-X==i-Z=vmau7wcE}#!OcxI)c}$<7_@;LUeCNcF zJGqg(SH=eYamrV4M*r!4JMzHa3bTLzF~@?s zT<^BkMA7+kZtgOIhtb5 zel)D;mf6JbVa*b;CAd)sBWlNcImNF5Bz8I_On@U&hI}KPAh~e3Cr*6ZT0;k0rW#C; zS=T_7j>yj=v8~$NV)c@`{#@202F$t@FRE>t5c{{opr@yPetinRU_yF?RNThchF*-d zq5;dsPn2)f)GIb+AV6P7dqrzIkEQou&vNbvK6TTu)S%`iivG*+y>EJln1qcw)4sZQ zO?RTTFH5p*_m{Nr`iQtLY9runks)2H1JN%$Yj;;GSg}XAF`Id@uKmdKgd@XlRL@!F zPV3qUGU5!=dbahGo#dpnP3V57M@A@c+9uh1nr`jc7tWjK>LZy_f>?$Y)%t|23`h9J z%Ze!=jVLv5_X;NKB<9YP9pIl}G@e)c@U_E^jOQ4scp z&l5baaU(f&NNeg~SkU5?vjbaUs1&U{0Ehhkz0RmCJQ3Za3nrGIuo zvC#?$ONclUt3)CzDU6lRC4_ze1|{Gos6uv0$AE6^kIm zQ?;7YpHn3+yy6wz>kG?Ukj9x?kbN6z<-?I6cO&A}j0l91i>4s1&A`7tclZ8Yo5u9I zzG&miYtI=c8dG+D(?oydDV_40yS6E!^b`4y8I1t4Q5f0;z}Z6kc{#$(=FsV%@Bm%Gs_+ZFN|=nO2_=gKbWN=U6kZ>66Vf zj0UzFoP4sG!kZR~bFHH4`yV|AOZ$M!ZG)GcE`W?0L~@Zj3pIPMN2eE0$d?%6k4?TJ z0nRf5KD!c-BOtj%Z+I#DbtMP40vRDw$tLi}sD=Yv(*E=$@PPtvD{fojar?wxyMOblIcHHb{6=nOq2bQ9|m^ci&7C4xNvtg*)p5D`vrivLY|%jdtGNAd^;6Y6$VuEy)RbQrt^XR5ngUQTjTGeh z*`2;${-GoMgVflzlo6$ecChqwBLOJdP!LC(X6frP_pLD>t^8x{k>M*Q!0i^b9QJjg z>{TPGMTrrRorU6CGT9i)Z(Gv`>A{nDT|jPKdH(oStiCQLrQjkcf5-zJvEZ~BRLN=4kr<1Zpf|=F^k#O}> z(%AyZIUCu)KueAJu7F7=p|a{lUaiLAu#$3N|6`F8XuSuIv)Y%{?MpNMwbU(DvEJw& z{#zght8dR+sTW|J`HX9d-O~%oj7=W?-WnhY4y0RdOJ<11uq(+6{`4jzm$$cnn;{T$ zca}fL9(2VhQ13|QE4}sA+opcJ^SVcN<{Q5#^&ctBtjdA8>;8&Q3uN^Hb^Ay#&M=SN zZ#0k}8kiW44w#J=@P9^gW+k@S9n^FDyE$|7ZoyLBCB#-PtH5EpA9E>5DCkQ{e&bN4L&erO&bj zL2URfuvpy>iE_D$gOOzhPCd6(mQCxI=|?ppUZsjom`yAGdMI@vhl9@gKBT~^t7mNt z9RxlZL#YXhIfCVO1shX>XQa7Tzj4j|fOWTrm}wvU^908vt8RX2dHd#SC#M>Z>>Vtn z0Aet{wSYROR?~q=x7|6FXM*O`{q!4Ppi{6@}tC^eFxA*Ib=>7dZN$~vFXi~)jC_l5Nk)z_g$AvxC%HL(^P zx6yXq!JiL|is#k}u9@q|gZpH|&Z0gjv^NdTuIc61kI@`vPpp{k4VoHTyWailV9D9s z|BnU>t;tLFqEmBPImHd#>?wZM=-}x*$SGqP5_8@ok2s%UI#<8xP=Zg(UOzSXN{xgG zUJdB*V`%upc4C3s*bQNav^syIF?txbEL3_bLlmOil!K@Lrug}O;no(Ke|Wx~lFi{y zU$@r;FY3+~{*nV?#ia%FQ)qFqWl9s78PrN-kUjkw;@;G>t;)#s$?mVnGWP91h&e^> zt_;>lgXhvnkpX0P3zGZ9E!0R%a`UC8&iYlc(3OT?5}>6bpI+;Y`Hgvauj2IejF9@K zqcSJdY9#@GTVI6GiCpHhh5QpXaInpOx#@$Fb>o4FSJZtu83Drgu)TSQgWs;?wIjiXzkZ%Pc2g*@@ARfbDeM+OQXMm*$Zg=19<$ohE%A^)B^# zf>7GQ0xxx0CRoa5uH)+alUqAt%sjKbxoJzmg_Byto9G;e9#H;uF6b%~lDr z|8z7FRd(Pu=F*1qr*#7XF>~=Ao^3b4w(Q~Pfs(xfN={SwM{-ltd9|9J^XjmKW$2ys zfXnlKVt{^!V$>}CUHr>G+)C_(6^U?A5*=WQV;D*!YsT%MCCEiC)LoCSgNk0+luYH@ zt}pW+`G1#^t@&FpYRiwue@(Bp$4MQ(D?tCYh83_S!uO zTua^n}57`B6tO3YL?Dw-KaAYbt!co60iYw z94a^&zSLp+s04RoGK3m6!$rjQ#rI0t1oJMmvFD>feHX%R^F%^^&b6$nQ_F`1^Y{s{ zkV0c|#xbMdnC`|9+C24|LNM>2V_i74CuJ8}r_yLqb#mH#>JqB6FN*1F_xQZCNU&db zPjKbe&_)slO`St(|2kn3rhdLv6DTObDmT!~OjY@mlA&#){^7DEat+Cck`8Ur`wa|V z5M7_vgFZrYdJ+7zhZB~)d{9aqy8*P`XVFr9|;3E`9bb=#Q@5WwzVg~6FOBd zBTa)##ZeH>{Y9Dt=UDl##%~<<`bs+%*KhgAChLS{=-kBUvg-5$fciZ70H3ZmbLrd+ zX7bIk)$sZ1+b!l>LxC;=Tspr{rFb{9?A4H!Z7#pPxNy4OfUt)4mTD6UW6*{llaW8L ztfiAMV`q<>DtP;YgPFe1^cmVKP2A(FT{Akjqt*Sb%)6QBe4Roib#YvAi@E%r;zcYt z;`8r=xs8$(2cX6Kh8flhnNm|(D!udbC=DJ1(4J7ae z^I(^)XKUU&fCcvERSa31ex)F=oRw;AO}yGWvs<75DSq%^_A6FOCRw|x z{tB9y@HE)pZILfC)Yq)!Fk)`VU{IUL$(2Ms`gGE=h8I&H+^Vb$%;#!!u#k-r+x_6& zs*LKz`WpwX!%R%1dsa;~8+X2Jv@_&^AIo#q_IExQwm_9IWtMU-A7tiV<94PL$lGMb zzzm#cx?MpSAvL~UCcV~q-3bS#inposfp&c;z7tJp2bcGL5gNK|ig5H<=s+q&;}U|+ zge3Q3M%cy1r0w2Osy?~0xmWZ!_8m>;L(ZmP@$h(|5<&kk!dZ(@^0t1D)N1x`c&tS~ zKF}cu(uQD_5!^)Fw>1HeZO_0xnf;Fg6+4-`D-SePacVZ}`HiCOCN_IB74-SUkxRdl z%-GR}0O*k(H=?#YVyqT~-edqM$+ojTF|&Q{iMeb~cb^=8g@+sK0*QqU;3!kiOUSd$ z6@FG42k()2p=18@@g)#y+rbQfzY%iRC2%!lC(&nrXzM;0;k(0bgAN#BM(xAXcl~l@ zz;NT*03x0mCM)M~++shiV1RB`WZ<+v7CjL81}0#4aA?jj!L!LT#xH{Rvp2>mMZKiT z2@Gyyl#s13;RCBhtcp(oWyz{cCZK@h!ci=o_&$-sZ!(Zhcd&hk18_9%H?^2u*cQ6t{Rrc zm?ODyr@10<^Y=zSCH?QUA~4o5FZ)?Y{S9qrCvf|LGh0Q)uYIgyWwKx(8Z*|gB_Rww zxA#-p7AlRCEDB&KPFty(UCs-F2Y1RVqotnN78Xp}E*U^iE=S(EUm zOd}vz2eBAezb~ljwQ5MsW$x?k zV0x>Fq_I4i_CPm$R56 zxFmNH!t_1?tXza|ylFtXywZSf(1sB9r+YTifg%Uy76F!_Q1@z@1sFNCzJrRfDY>F( z3W=X8TNJd68Fn7;=ruK4dMQ>1ou%1oqN)%ZoK#b~Zzp8EKxYkGu~i5uwjF>^4gp*C z)Y$8UTvS811!HgSy+SVZ;BT84JlHA_FA(x(i5GWtM8+S)S1Gp67&-$?I9lq`+@dw z@V!vwXqi}_?^*?Sv{|hmMmYP#A=ck_sdNHgnQjCur_EwhQ#Umk@GUpzkbK{-#V;S*?EJN&VE~4bCJY-(6mj#kLjUHn)9OIBL^*cj!bJjW=tJUZ-!PeJ`+N8> zVYSv4Sgy~$o2KHDeN#!s8@wLlk2Rl|M(H#ockAtNRKTlvo zjAnRstD9soqLXDnyu%jngD)kXh3e>z*+4Ee1~IMOA1~T+ZZ^l4A2%;Z*m~>qLd?ix zx*vp>A6KNMkCn5UBOOFb-mOoN*~OE;8U#z}*p-y;n!TG5RuMMje3k_BCcx`u4{B2I z34C$kx#;vUch8bBcIFHhQ&=I$tUNf(*eBnQIkfN&Xwe))mp>ZcqU!x*_a9Va>9eO8iIH8=bUM40**sCQU!=X;?8va0 ze2;QZ9!vVMwN)^zLaBGAXZUL%<|AnJuryw^m6f$8sx@`0N$5a%5Ba)`v#8Ok(njCWG6+tj&yG7}oeZ5Yf$rkPq1$ zb8N{|Rk3oPnsOxqR14E4ZoP6>oerwD9V$f77MsgDOXizs)Mm!8DpcoZfM>9)$MZG@ zwDgiQlG#faT^K^yj=N4)sWb)~RX#4acd9;dUZ1McT~GM|?YDVSyD8+hGlm{hiNfJx z2dtkUijBcG??e%R=q#82A6k?eH8rGnj#d*( z0n>foGVGsv_@K||o`k)|t#K72VB=!t7qX^K@`qLAi?>HptUH=`wO(W`BQlLnQmSa; zX}Lk#r79SrXNDHjFg)*^Lffx<)cUz5q}CPDiIM|j*f7D*6pmSsfq3k@c5LLeZjc`b zvO55iNPeo)hESSgqFEw(RbxBBREY&u@{YHQNAq(hq>ugkI7a>z{-qH`FIYE8&4vHu z$7rA#zx*A!h3P$fMl}+gDp67t;Ask*99_SYqIR8IFZ$`&KYzni{n{5D-GUNr(scB! zrV96-PGvj4^E25>59xCCznl-&PO2HT^MU?^&PEPKA!Afmaqc;ytFJ=yyuug!1ThA2 zAwH=X4zvmu1gXmpin6L7$-}-0H3kM%ol`x%wc)ZFn`n9g?q-QbK+cV)stG$caCaVuxwWjB7JY`3VjSkeP@T0XtRYOj}m!3#LI5$+)m?K^N zPgb!DKk;>89a#8eUFm4BAJg}E!vgWVYN_ZOfKaiGGiM#Ue}(#jwK?T>dI(0P?$A%w znnK4M5;3_bT=TEABVE3sYK&Q#45*x{PLDdXer4tt$iuR)@K2d~GtHuU;iKA_07lQk z`JPs;H=f`jjFF#(F|irs_E{bnW)!fzY~X(wY(DZo=w$m z=R1u5Vf4PNNviDb-NK5dA-yKpebLk;kGIy}f2~YaVJ$+|#~hzc#cYKfD0@8RGWAr+ z@4wuhAWLu!6vZ0z_Gf37om&5B9v`3jNJb|6rDyRz7)clU`t24zdGqzOZ> zgO&qTy}&$ZzLremeaT!+%%JIR-B|(3;9reD#k%dEZJlW2c*HxM!bf#Y^8oq!s(^3! zoxz2SLs#*pLY?iL?>AFJ_h~!r=K~FIs^zm{&y^)$MC{UmzyQ1WO7(paL9c9bx~Pu2 zWN`MbHt?Wqbb6}4(4v(Kh5poYQ7_%;^W0FRq$3kkeW^0cdyZ)g2;kOpp$!Z5b|6tb*EvT1oZ1{>!#MQyQuDh9k z(WTzOqsPjS%(`q%FPgvGC!+J~I6BhGm0qmnCX_Qvu z13Zy;ea^VpulhpNYC|yH&^P@_A2=)MeYW(|@(@ViD@NO77Apx&Z)GX3c7c4-_=fX* z?!K`MnwAd%k!`hFC6d zTl1WEC;V{NNz_%f&h{i_VLH{IWLBfpjn3qgpQx9{_~o#h1#EV%Pw+nmt8Jc29)wb! zZ%4fkIeIKP;Dyl+Z~eT8C#n=p@$xxT`@p6}S@Sv)?j66Pz&fX}2b&TJrwr9(P)1xm z4{}+|Q|zdtjLeYLKFaO-D`L^&Qk4@(6Ir^<%0UEKg5T;aaO7&!thDr)N=R#0aRc~= zsI|fUpKlm|jti{{79B4!?$go;J2veE~8CA-dCDbMM5=i~;4B z+}}GH??zu)1_Y{tH+rp&JcJx4eSQrwMb77LRG!1=Ybl&}Xs8qU2>Y#?-jk#1r&`1R z8F!F@>DgKMT3}Y=Xx`982Ja7AA$r~m)2t}V)9>X%yc}|32d1q{@V@ZLTxQ>}tm{z} z24MlSXV>;m4;8WV%J zrg`728|wxykEh)nnn3K$`Q?Gs{u+AMm{@cGJ*KyT@)nENhQVihWU*zo6*Hgi`o-+V zb#eJEp?c&~S*V`+j#QJ?PKma7evIV7xfY<;$f?iqB}qEGPolc>LjLV?qnf2|yePNc z;1e6sV0j(>PAZm2di4MXO*I~WqsHV94sfA6FxEx8?i~7m=_p*t;p?Z{sE|e zI^dO`&S6KY@qfgcSWhTZ%p3M5A$+fi-#!tpbP-*C3Tk?sq)bL3UEL_NzMH4iN0zk^ zoLOnT?0s96Ba-C(Mpv30U(UWDtaol_kD}`*&vdv?dEhc^U7L@E@srMH z!28vL>+b^!+SP(AW%gw78PKLA7|h>Qxudm%q#O4enoJ!1itK_pz?JhX*VAthJL;K1Hxs(=VY2=5b%bc4;X3VXsPBOG*8?pP z0O^R?ftR@bF6b206~p%36I6U7|CF?W6zhfqPKLO6&(^k0?7XB7d}NO`x~~~^7qW(% zF>j+OTq9Sp@5rllI-#Hobn{{DvnwAL#G`pv#+L{m8rTVwVh@?7FC7 zBuT+!s$`=-F?Ou&m5ATE18zW#j~HHAP}EXN=#SOWaqGi8VeA_Twm&ml6&ck0gMiNl$?*Cxnc8tUWtL=Ob17&eDX) zG)xenE|YeGPVWoL-%fg2+-G(vOoi@+W}|a5)ILIPKFDi;%^IG9*FJJ@2(*EXZcaj4 z)sqgQnz2q4$O2t-9zK;ss;nCg+&RteWdEr+m6v zLBBWe?hOt^GI$w=)NEN%Id)rn!wcd(66NJ2d!nGROMG zRs*kHAjd;51Z4uwtX9#=2W@$-46BccPB0HJ9zuJOihIXCmY5tIQ{F61!G02;d;kiQ z`?#d3hll=P*@1JP#SnwPkgbr!!!vBFTxtmpDEUB`lTGqLHet(=7*~YnLewWh>en%7 z$4GK9!Pwo?G>Fl@P`pe3sNx~?X0pPbuIC_n!}zC9;@YWl#UE`tZYEbSu7xhD%OjH$ zyZG@`nod>OXlh9J^X;=t&nMTXxL2?5Z-)9}Ceax7JieErx}Z}wq-}A{E8leJ5i%zl zp@DEK-xOt5nI=}*YO~6cL+1Rq*EolGX)`l{DAhRDHX)<3c#q*hM7(sfU)Bc(_({73wA@@$B2^U2yMB1AQDot_ znaifkgEMq+a+4!&=)mjG*;9V|<*t~qi6xx0PGv7_vH=!U?o9sJ0(;n)o>RG@W(b}7 zOhCa`wAQBWco_jfdzx1c&W+U02uw`kHBXhC$WUfZ9fmA6%SBU7J(g8{b6{bNlI5(! zUzz_nR3|?{(-)BW(yBE95*EP&Z#kJuze{Giu&=XJHS#v9q0+>lp|-Tt$pBuz z$%<i=WoJ%gIuzOZkVqZ~y*K>;C zS|S}OL8aFadaog&BfaF&UTf{OuIm>F9G9s0QNXBj zyvEX=kW)4p4WpxvucGaZJR4fw<1p-yiMnF=bnpXb`tZ^{u8qhay`(HJI!UA0sVrB@>LCGyNc^=lcod53py1|NplL4xA zX(!Qb)28%%pEUbX=i>OA?~NtfH54F!cbX?1 zFBz4qRTF7O5i)~RXF$$ZK7J;ED(;j4CkiwX_44Mh{^K@d+W1moL7-6g zbc3CHSuv-4mMl6p&EiY6cWMH6L2O_8@;@Yt3%c#|Y_L(rK~WIYhr{hfWJQ zUQQniG}{$?ZLLuAQ3aUS`gu{&KO;5`a2N&|$?k@`Pez}2XmrcEl#qtZC6>gvg8Y{j z>vZMaZBnHkEN~qzSI?C7sgWDIH>?ww@^l&dl9&T;*as{7IE2pf2hCk2BY_Ftj1gmP zrxObYlCa@D%7@~)>>5hkx^;{1C|_rd-&)k7p;uB+GTL@6`}6>igCI%a0vz>dxY)>NWfncYgA`daO=&Ndp%*%y5;b)@=tbe-o>} zP}1hXOrFnh>+{k$W-XpMvj`Y6$jX=*3fiee-3&%`EYa0GU{({xEJnMbCA1 z8iJ30EsDmTdhEs=R_Az3j7|QjKD9`>sCa$hrB8*#)oesQwYM9f{oRicgm*rxxXR&x zv-DlO**7H>vEI3(hdn`X)~5G>Bn}xd?m4t`^Z%p>x~&@Pj8S&DYO8i9^`iaS9^zPn z55?MO%3tJSws$%x#Z$;fq${A^YhMO|*4_BnGqAv~Q(1qa!MFbK zR*Rd5#m~msX!zju9ZJG1$v_L<}CN{G81^+Ra#ppnno@=IV&G$rZm z-=bq)TI*%{3h|K7cHB|aF2|?eg1rNrFHuXivjwqT&`XJR2*-!{`iv1Nx5yb&0IAI- zP}u^uOVZNDNP!aA4&@*#YsI0Cx7R)|W(P?r*v51FdAMT}ARJhqGUcuFoV?>B(qvUeCT{8=z7Fbr zMn5%@_%!2$8l#9yL^ZtYL>@YIg#iPNhwL-$ zWZ(M~8)AE9Qns$yFm2Qs;&IYoTd>F2<4K;~8|h(;?gw^N+2if^wh$ouv1f#t)H*ZZ zkEPFsc>68|UYxHV3glZoWoHhoMt%`qfKV3YjbY5shJ5#uSW#R_ei@Qmr^|~N)Z@Sd zy+vAv9iSw}RrBnbjh=Y;$q`z+>ZBfzN^@`eUxdRUH=v0zE$u_sj z=F`3j(?&N8^}|VF%;7AbfU4hR9uM!X)9yZw4ae8*moBclcgoqGUbl1f^D>xOV@2h} z=j`aBRCl*M2An;4NWgMo-x>iEKSbwl$*y8Dm+@hBzI1?mY5tBQT?S7t<;bW}zj20c z&SRPHW-U0B`~at?@gzR?_ z6{GM`-@vt3W#wqgwwLBd%?NWACP#b#3@4vcH|x3y7Ar(nO;rJfW1@2-e8=urK=MD! z07>s2U%|Aq3cGkn98>!E(shBqwi=v!TVG#avghyL8{4BtMkHBU3{SYo*R5PK zJe+rl2l6mBKUW3Zhy$)|hHZ9)Fc9I4F}FTIH1{Dlkq zXB*XXIPawv^2}G(zNfhJlnsxk7*72?&`xOa+2MH7=45hrV1x9Gga=L&ER>8DxVfZ( zXo>7jt~%UOqi#atFmnA#QUdY|QG0vd=O8~6E-@mpdw+44kmTA$95?e}*vr!wVv{+T za#5?K{%b-yI9l|8{6bgfB666Hf#u>G-Af|7Jra~%qHQJ5u>tjucO?}w)~7&yGBYjd z(5NDikpbC=nuHt8#=o~UFiOOP=l>cydqFba?KK`s-J%9PeUVkNlHWv=c>IM=*b9BT zK8>r>bQ?#(+VIQHU-DhBGQX-8al^(a1z6=0X}H6+7B_*zTgpmF$7sqPTW{=#N{S3L z4B--}PIk31@+*P37!8k|>rEx}b2ZMj^nG{h_K)`DbiF>Pk5ZL; zjao}sI3K^l0LfgUnH@cGOdsRB=d{Ef|JK>OJzJQj zxyZ2mJ_h){+4%OJ#@B|i+T7kP)&MN&sAu`i%0c8KlFh*F*OC8$bLuHH9!OOf^$x$N zYOS4t)C!*El({8votRL1yz|bIM)FJMUsn8)G!evkhII1!-gkOyTU*dz99{}`aFInG z9NIAm`E(FUxQ+;d@&@GFUaeBW;L2j_nn&nPsoI{gJt?;hn>m5A7Hlf{te!b_tD`$Z zp~|hSHskaNo3YicS#Z8GHl;zW-On35gmaol0W#hSMf7_8_Q>PQvs|H;AuHA0c`zb!s?9Gc30bNN4~0OZm>Y! z0XS{ekDl%OKpR>nkQA$W+{m~M zo4U=8T*4CI*34@>RQ5DmQgDqtvi7iYFC>N8ne^N4rpNe?{dh=ydOS45(&pEd ze@i6W$i>wS6X8ocoImoBl&{l@}`g6*i!-8X4# z*8QJtLbd;o%GK-si)BlA!HzIrrI`ibY1Q7!)E4fM!vm814xhrJXJ|O9#Pq`~-Nvp6 zdSw>y&;niGl^c);Jg@fTuxrz5PVbF`*kS8*7b3lDRbbJwucF846!%lB`H=2zTgTbR zJnU52N&ucZTkI^GdJMwk#! z7>~q5mxub6=C78>0xC@4kzV_eZR(20#Y_{@`)IB+StD?&$lJ=Z>yM1ztc%*Vw*26g z6z@fm3CDbL9736q{cJkLdEAk#5C=Zb!K&5m-h--x8}EkgI>TN^t-yB5{2SoS zir_h3`2}wZ*AS) zq(CC+_{*lmLys>1e2ykBa-cJlbN5vNnK1g$hepf9am^0w?UF1d+jm!I zdH39PgT_Ge9gU62P@BA1buK*9_~tO*?|Dm^_h6$|yJN1`M|#r5tRVd8{J{RAH0_U@ z!`;5jyxSG8%v!_qpXaL`!!<$~QnVUf!dsI~S1xOOFSPXo%wj|-H+0)Ci@&B#)Z$1@ z`**J*9ahSoN$qO~$DV>Avt2^T%s& zST>^kp^T%LBt)i*FPRzJpaw8OJd~k%$C{@-d5by1VN=!fC1yFnOOeM6;8(#0k{l`J_E*KmhD= zq=K-Cp8NM5Ylr{*tD1J3C18L+syFzQ!wL!UpliOxPs&v_5j>nQR+wN4{SydMY4 z<*Ub0=mGP>onA^%me_{^U1^H~`D(Mx&`JGRo`Ri9xouPAXY`Tdi}!JidpBfl~vBgjg1OP?a0eQoD~l6AB5 zUzBE{vu-LPs*jH>{xhy~FUjiBfDVq%uqKxG(#Mi|zU#}plV6&!oMom&o9f1T#%qQW zK!;W-^+Ra@ACM5S-Xxs<%Y?TJ>HHzr52!N!pKw(n{~fXIV^jej)3Le9{8d(&1{<{d-`@idEV1MS9!O<9Zk z^qfVp4c|a%!hmN*htY3gz#o|wi`T`tj4F6(qOH}KZK5iRyf(*5=rA)nb!jHVi8K9< z`0?~c$aO`4ib)jBQvQpVSF;tPN8^?Z>cqtfCtpKR2< z5Waf}0N!$^qhB&AI%yG_35tH`p8B}=-lRA<7Po{R$}m%xo-s+DgNMe<7hb0`f{)?JTnjzxpWPK-$%LEIAYdnp*D+0Cp zDwLP@o4S*{bAor9I{T*3JkU3GaU7-7t3r134fN=zAPkTuY3u%`YR6NIwX{357MGDR zqQuHkq#w8o)TkQF@mvn+?41)ihB7==gP3Lp5icBG**w60?1VSSma}(f)gQD4r9^na2fGQaitSq=yzvd6?cRf)f(Wlr`QwHAUh?k6F)BC+4g6B5CsI6 zF%scB?}$~)0I#U9CK#T)=z0>H;{S_($BGh6JI17Q8Y4#u>A>sJ3XV`|9=CSQ1Ea;n zhQ(%-AHDBYt7TyZ&q&T*)c|8#aiD6@Fc`yvlaFO+?hM9Cen8!v{{G=sQ@)DRUAG!$ zJjZF#A`$++J?GeR#nNE2*gaRPG1eQ$J~u4pZ+Vu=8d)3n99&5J6{nmhR`E)DT+H!P zNbl~QCRe6I4_KEZ1BsFkv=%ekTdZMC2|x@5@diap%a^ga@vQ8<%%@hz%zZVJR<7j~ zna*O(`<_{W`nxn=XB9CApgcwS%9WKEVY1TzL>2J8 z8$-cg9~9r-^^es|c~-_!do>XF-zpBD5HzK?84Tn7P(LGuX^bE(ZMt)P)U1RK4~!6& z>_Bs|nPlRFIN;_H&nBd@w88G4-)PC;_-K|$@e6Y-Z>V6Dk}-{og$E6+63fT7XF{rn zCzQ`+eemT7&TY5%hpfN|4)1>P5n{^^rAFS|?#qzG!l0%#30m34o+K?_ zDEUp!O(o-f?7*3`*pOPa!vlXIN0QM9=XM|7+d+xv@g^3Gpjds12G)dxqCG`cC7B!| zz;FHHsA+P8ju?7HBag{4^4;C%ufaPC?72gcG$u;J&|}k-^>&@yepVSiF>g~NmdFo} z-A1(NdGDdrd_G?m*Kcgy;yp--L&*%f>}1Y>D(4z1n3FsuSEQX)q!qf)dKIpjpn?OO?_U$(TpiCB?{l}0~AKdc_k z6?W}t;>QQL8C~Z^FTMb1V1KFE)Q{;;XnMag-&R7G^|aD9LO6FPD+JiJOiz0j7J)@j z;{3_ASXrb9-PjiArK2Co$M=o{#p4EBAc7C1|9WNihZ`yu5r^P>*exNU%!4k7kf@ud zo6|0rX7`TZ#9;M2FVoekcR;>DA&#-8&CT1MJGbc=#7^Ll@W5)Y2KIAc#>aL|`@OGN z+;>`$Xh@fws+^U6I{jPBuxXc2Q^sOH|JFp5ylcy`@U*@~Y@f{$^Gp2UaJ+iHL%$0m zw2D!xcj-FNXg>P!ykCPd4dFotJe$`t1nI48s|?i#Kx?)0$qlbO!7lZcVfg^Y?nrfY zhpw4nmM+wvnt(M2rQ~&K)snBj0#L2kFz00FfZ3b)=R7Q-sZ?{Y^l6t${zD%>tb|QH zo!E{C4@c^d16Ufc7bmSlOFfw~<; zW??Fr{tW|hI6stWi3@>6aDY#;Ms~a=zVS{;ESs)Gj?UkrXVyRuHF%(jpXgJhT@rzi zleluQfH^L#fJRqBm>EoWs)()g7HiQTN|qVJ-CDyqrT(av>;pMmJ5M)qBb=G^^F$p?^9RAu*2 zGxgutJ=x)KMKt3vRa&lpS>Pz#GBw)#NLBmCWX_rdDEmpT`BX~FQFWr4=X&$*pN3E5 zErs8Hj$EhDvSc}NT(+!DSm#X^?CfnWN8$uF|5VTGeqy7|8{X9(SAc{HR^C7A$Q zFK^)PUOkBrcj}{;_u*)^aqo6x+Qtb_&{ym&$}~1hgSW$lPk58(dK%uSor7^;c|aRI zlsgua&`eu;j^oSP+KQLCh%JS|`fCFBs1ev1LIj3+#bbH0ZqA5aI0&Yla&RJO=b&Qu zlpgZWj#x$K(98 z(DAY+pczQ%TQ@NI-&jdQc&A&z#BhGJQsfS7&e3;1v=okgn@my=)$nn6b;>8hz`xiT z&+6ENsqQiP8-0IRZz(y%*dQgsesEtpVqD)AaDDR?Rd}I2qsI;YHqj{6JA_*j6YqQ9 z1#=Awt4ZnfEk3Sxa~#6Gk+D%)@SrP#NspZ!jaf2fTKj zh2)d(RZHy&mz)IGp<1LC-%m%NA8Ap-B-zhWIINe|x5e8dDdaOpCsGcvTdGrYPH44?nCI-Yc2jnR9gkIRAh$ z%*Zt>(8Z8ktx>01GfbG#T?WGOC`0RCif(55g7PkH6Rm;nkLT-2RZ2wz< zrhf}Naz+)r6aHT(u!QC>`;QRlIan0z|BrI;Fhi8H*)!>SEqgz|a_91JqQauAWP=v{ z3VLT-9TQ-xqpFeXlS86V3#UO4I9TJ}@}?-X#pnW8RL~o)PZ+g^lGf@;UB% zy!R>97k@c0mE7v6Vgydkpf-@DOAGjtC3XxjL0q5mg2yP=A82`?+>3k zwXC7GrO}VM6%UJ;p~kUYGw5Vtv{@Q8a|STy*`XVHyr(jA;&j)Nh#T)Vnhw@OIJ7B2 zE1B(5;lDK{81a{tYvULuQT42TYUu7mX<7N4fT18Y^Xtn>V6TtbWxuoYwkw3B}!cRGV@~V-Q!1%io^9N?CH2ll? z_92CZm5uw}ZiZPs??A}XG<#hDIjG{H7*M8nLfnv-MDP!jImQV?kwwx{M{}He<7fi5{bA#PXe0;Mr%f3GF4jcDKtR~{V zo>O|OzBDflK_hs)04KU!0rI)WnvT1P>}%KP`;^qm)7mmK|6oM*cRb`*8=uZf)Ysio z@%hj}V|d)wErNH^sxMX)Enq2m7t_#4$uWi@P)KR&HL}>K&sqKbo3e%C-IUu>wrAk+ zTm|CjaLH+T!&OMmSE;r*O@MDPrTY+fGpvj#=SJ_s`SY@i|%_s@5RzpoEOyg}G~-ARTgKkd5bg)2E6k*b(6>uwXb z?2=rHgLjr3jC$eEq?ef72SPkNXwIg8){VD3b@@ZUmw=B={|%|qh#b*5=eXq&yxPO# zk=-969lX9FO=XE~>{TAguX%KSKA+t?xoB=)OQ@|lwV)XO&TGkXSr&Ax6D+jdY)d&9 z(!oc#!FJ)094FRpSMwy-XyTh*_O)W@Jx2l$#XvWAwVvo1u6~@oEHrsO`;j`Ttg34m zUCN8wh*BZFv5~V;bNr+EqY-i4uFCl=R)B3t|}yp}ceCBy)@w zGY%IsCAqKe(J=tmvYXf$JWPi;X8W%txsL|H<@TW9FpJm0yeAKUYi$|w9dz;*p4r?B zUz4lbnxv!Ood)YpTG6xLkocnm8rC1KowX8f`a|EUH|i|%TjOCpgXUe{t$aR>S?|uyOO-_P{y`00uqoV20(V@}WDS5@_38cJ#kYH> zU_3?UKPLf{K&5GVJA}?hgitfDT=wQ(;Fy4kM;yI*oAbh35g9XH{LW(yi@SBR(Nd!$ zV2SMv1Sh?3&v{Q_{RFaX;u)wPJ92fTaJ8$9}Wv0OayzHrgZW7^93eY9$H`pq7 zRfuQd8=9!hwWd|bJ*qDrK~%afkIc?dm$A-xo4mSlIvPWY9^u8f6#M^%tHD%Bi?|Ra}Z*#qepy%t2BQ{&*NWikaw*A z!Ty0kNwx%0OPRk!NB6osImp$iKtrXfZCNqNzdExoA2j>>tp@3Glib^+T@-*YSc$n` zy|HS+LDDWP(aTU!o&6n(Oh0WK=+YE%%+2_4JG})r`-#y#>d`J|c?)o@{^y;M6zoQm zNwM!+gXu5Roo^oh0RT2mx$e6y4klMbIrc)cHqsG$vB%#=dUy6ao{yz#RPdb`lk&g= zAv@nV@^Vb>;18a7zk}V=7+Yno$;^A;g&@_B6)-=?o!x(>tZsC)SALygA;u2ReQ^>CD;4na+8E-J8ucxsJB++wPF%v4 zjkdYolm1icvu4OOrn}Nk%IL@d7;5_}p#GF*DS|0PR{hwy^Khf-2bcUqlm-TrA9xUc98NhvP}qR%MsS9P*us05)>tTS48AgUf^YW@=hCh8S3?j-$JK)@d~1YW?cuiJ%3$nZR|4pt`W$9(yE$W&Iogv z9x-XlKrf_oYg0KTvMNj2ci(_lzjuUz7Cxz~&G*YXhFWq*AYXbDf9DH0goVhKTPbqQ zr;EM|#0z3PYygDf%S|j}pOa35@6#jZgQSnNLBm?w;NfB`xq>255j&tKrFjTpmLv0= z{NnIz3;`djUJwmS{AN~X4x6?-{}bew@9(O*j|}dTcNY0UKlV?+>1v=lyhNjFHY-xC zVju&_z5z^QFarIIxNc1ox^q9_Prsh#9#>5Y$AZHA@R@=bSquG}K0ZihN2SwZXlrTk zdU(;z45NOb67e3AXOtC#KVk1F{osA;lvG9*icKJ6T1R^AgBnuryq3{?1~!7Abr1+l z1Uy|3?u9cM3~`L6FuL3+RWXVR*1@f%t2k!w-=%J=iI3D>Y`U$YJNMm=lE!^XCo3Jj|3!2Qe>vPB6O7xseOiXNzrY z42WoxvsmJYWZ)Rkf~t(z;1(^31x!JWiSf79!@!Gl%yS8u=IVq|+|D1Sgi^(u8^pB7 z5}HVMieRyjW}C9eLyxRJs8MJlM`(P9mw zVkIwqun=rsZ6UKXPGD6;2z?Yh`UyKO#0?@nVsaHl#DYi#`2?c!O~QAgPVZ2DJ|XlM zBMUNJ`kQEbP@H-+!Fap4NU3h{oqep%jfBEzilE89tHkQk5y`z@tK28kl0<}t3Vd}{ zEzz-L%FTJ*ERn&%PH0P!br9t2)>CaR>*Gxxe5Xkv9_VHCbBqek_+ftgJU5oe%(x*D z75pnq+b6pT%QA+Idr(KpF_FZTzs2>$Wb>M!LO717=De_~xF{fja4GN$;9d&osGrBN zTCt>EtZLq2{zLWPucDWBC5lr~1Vw;HmSTh&HA9v_{DpZT)@G%sK&lUjmb%-y%ojCkvCif4c`SJ>#%DvbOX`vC|IRPBg!#)ko4=D*+W)z0 zmcs3FCzn_vsNvH5#(qhFDMIhnozff^^XZHvz5NCf%SLW&GZa|y`yu;AD16ff3p(P! zv$&cLq!Vle*dkn)1qu0}BfS>&>b7rY+!4~FdKsxTudW0(9^8$ceUq>n8se?A|2E;> zQ8D*ohqS8cTg%r!lHz`sM*;Qj%s)@s@6fvVDIP8EIR8(8>noC?=5dbsr;w}T^KX;N zt@>p51@Gk)-niyI{YILn|7nVEW-=%mK@T$jWee2T^$ibmeHHT-;1eZu6VX9eu7@R9 z=9En|4Y{AI5F*Wt{~PnWDhD0>X*+M-Q8(Cb$Qc<=i}t!6N?_@BLbeb;!d zRiBTEqZ!KM^jQ4F3~!Uuc=_h8DHn}{-%pEUdSN*pw#mfIyjKG6`3fw?h3ZJWG5c?; zwxb;%^EW@|l8Fig55J{_ggtBHeOLK5`lPe;q$ilP(#zcb1Y&lQJSRQZS`;x$+(KZLhhssPn;^KZxz~fUq0<|c#CW!4fL5d0GD`0xpU{FS(4q|wS{79D;g5+pCD78j z>F+v0%mIha9ZmUD!4^Y8HfurzQR@mFw-Wim(}xJ+ALcA016?_nHV&+8`P3^0qfV{; zx4`8LEQ{+vP)aH`!u8eYKU&)GWSLQKD|wl*ug}JVvGRXO&=#AKXLTWHG##C273SXd`z&SW3cIic=;d{%WszVl z;0E!z(?aKwoB(#{Q%$(-7N?=-)FVjY&@H^3`#=DC`ron0C8LJ?>SK2 z(=*hdkLHMkudHCZSN*53QtuVbb$?Pr&d{}u>g0{G!0#RaCli<5>#p_IQ(cao=M8A*^SM^irIXrL{^UA<%D{cq{m$sUNlYJ{j}O3>$UBUK4Pn1LfO{91QxD zD7f^6gIch_=@&vaP@i==3pTxrZ>5i1NExF6;!d|<{InbE3%ioObosp;Mo&2}gaB~L zR=Dm0nSkmMDmC?(jLdQ2wn>&n7lr}1$i$7loQXpDmS(g>h848tTcF;4j)w0KL?A5= zN;wFwV@J(w$&t-?gAa;Kacys614j!p>i?XsBWX{8a{v)#sI5U7w^{;=28Y!>$lfly|FrPMB{~*SO3%nS+Q&yC=^D~SE z`I8pbe!P6Gsmc6x<}QzD>(%^-%ZaKcDw(KD9CSRZ#6QY%Yn*Bb)hFyEA{Lf+M;i!R5%aaGz&zVB5l2YC5-;K3!@sz4GRc_clcQ$syPk5Q@ z!oNu1qo)!F8~vM~u$6a&>Gj^nzJ;vl%A?Z5_FM8?7p#ZaJOtr#ZFhY*;o|t>r*51U zve|Q%VkP*q|`bX)58ld%5kQVkRFnr zKXR~F6Y0S?V#Pjoe#E`YE~PgOJ`20fC~_nTDOHynQ&xz10xS^nrPW{`w8Zu#a1n%1 zzog;icECO}Nf=l9txwjalw8?137fEw1;{llE`zlH{fypcPA=tj!voh=$q$N~ss$yD zklAHTf^44IRzO~y$9+usxBHei)RIfpdG?jKyhoV`#bavc#MQmSG0WOC+$g~cf70wK zr?D`^Dot&0@8r(w$rn}Bt<7Z3A>2*@&!&733+7`^BfTXkbro`)2#&m;FYmqx)*sWZ ztVf2OhwOeF`vpHB`GvPB(UOZf?qR)jZ#rBRLRC@0+yR|DR~^e zVpl|w(&a9netyA{u5_HyG$#H<ZGnG_mjt7Wx{u%hH#fn z39bt-xU$@~XzlmC_J5jxMsD1o2#x1p#dmaB_8h~Hf4k&~8yNP?o7ssXGuUi(#Y)y7 z2D~9$FXtF!6v6`R=Sb@cG9N*=#9Ic$8Mxve^Ar>rN8f|`co7}>AaE_+nnONB(?=7( zbB8zY?GM8pt7owklSxy$$yEvcO$89LrMC3V7f1TCi7UFh;eXPDYdu7(_O@L;*^;fP zxQHrEgql~rlu^R6>p#y)uqTuCcNZ?nwOt}s5szw5V)k@GF5`0xt!rD__aPlWLUYf1 zrVlm}doRO%4R&@VB&IGv)s@kV5xP%T`>j87Y&8eO!b+akb{2wsu4%OzoRak<`VOZ)Fu1UJ%}6mamzYltHh%G2(aqDQWD3jC|4valxLb+SY*BHY?SR9LzODH2nj)eNtxvv0s;>{3Yj5 zeZy<#5*JHQkZlTw4VTn(~N4#V_(v5`mf}5>Or+ zR;JPJYwfL!1?=d`mXD(6N^^hJnl+iqt)A-S*-pz+TNT{K3h9BR19h7O1#%)%5m+%1 z1D0-Q8;`XA$#7aV$cE%XpJj*1kazdQBl74D^MsK(pGz1CghQn3{JlOzGK9yp-5v)o zJGBb$OaeaH(V(Rhi|lc|en)d5gs0f(5GggXN0;RG=jo6G8$)Bc zIUk27UPw)Ph{7v+fd-gjPZi~#0L}vt+IJHRK##=j?Xg5I+E-nxmj(^GhDj{ z-1k3snIwg|S>7)xZ*^TE-giCE_$Z0YH~Z{$CeaykMOtR4(1YKqE2(2|N4Dx>a1FUR zfJ@m!_Ikt)|Dn$q%N=(;h*Z^^9xMla+~V)5`H1OexZldTT$sh4Xsv_gvR5qY0!qhC z8Eh^6PRK92r@J&YpnMe9BruZ6j3sELkcYu?%!Pq?Xu!JY4URE7TH^j*~EGXrZDu; zh*!pg3ngAd#Eh5iw|nPo8Y0|8yoPxWWQ2AZrp#|>op2}_@smIJntFk1BMHHjEMU7}BB4%rj&K6bdmcr<{^_Sdk%jnuY zfX*IrBAbeAFxqPJ{sGy4&V}l@nIP*@$gSr)q`)o!zvjLb@C$Uj-dehmEw{cZyf_S4 zfep>{ZS;nChnMLf1u{DW7tP<6g?A}VzaM`xW~YA)+dtllmGpy-(7LYn%y2L+o!Y^X z%(XOj9qK$V`II(}6U9l9(@pqerHvzzGtp0TsS{(McyH5lgkf)!&%H5p!4@@Rl*is4 zcjwaNw*6G8S{>!5c!VwL=g?9&nzg%_vULx^+(mJ-7JnT&b(I0JDFiQq3vqRj{v2kc zm35X}&lot+tns6+;(?_jFar@3GQ~wb(GGgDW{|^EN#3m65*Xa=Fu3TQ{wo1JnmJmE zPnOT&>3&SA6!mqQ%SGHQa50grd&MK04#ENguZWOVFu04t=e54mjq3w5bBGfOss-9x zq|T#M)Z`r(^oSne_LKXfj2Bt4RTPW%blvoR>azHN+E1V#eZRlUsuIV#CZTyWFi35A z!F3*9ShZLJ@4=yaTJ@ZYG_F^zdIsKbz0;*Wn4*r-0XjSB(0bkBke$1Z5o%}i+Cb+! z8}wsuu7{FRFf6;;JV&k1xg*lL8O9?|PrddwZ`z03*17q2o}Rj`9&BtJ`JO_(+-3nl zI>=t2FNbOxyQ(M7pxwM`{9=hs222$}GdEO`2e zJK!k3b2P4fajbq9(>g*-(y{cZBl90u5rj6?Ha97UE}d+eoOU`WhD?A`$`ca1J%cyp zeaAbP*@M}%cOYu2maEFu$rUM)66-mekVXS|O*G2pp=G3Zk5rDMZX~N!n79!?P+rn) z+n$sO8Ym-&jvst-tEjt>U7T~eGW7jxK7&!E3`8#jF+3z%_Vf@CS;eaQ+5p5}zHKRQ z%TszUk~BPCFo_7;Dy(o5FXVNZ-On$qAY%*f-7SrF=`6%m2@G?e$y=V3ZuQe-Q>$s9 zwQuUlQgfD*8|=I#SFLR{$}mZWl{FU(0wezdi&z7&ZCieOl2?XCCUlRkToexa>(goL zk`k8~N{W1bExL3~w8>l_8BbGiO`RIG?+`uA68rPIIaZBbeQ)SVHiGK8%>jFhgA|fU z00o6!$<4q2SKaS{&f@tn_34K+ z^-1LG_Gas$dm-zan#aw`v&83yTX>!VweBCb^hVw_u%n^!J`^%SUBUZT=UGhg7CvK7 zM82?ed#jzYZi|+eOYN-A?Y`@M|MtRwu2eD4uG6kHD<)_ncDM-!xqoac3;?bm4UDOu zI%|8&CS2{?Pji6PM7G66)pNZleig%grn|RVuKVvoa*B^Ld$Er=km-2$Fdv0|;OUZY zslCZ$19#8I&kTiki_Xg?%2N;88Z`tuOP^vUtB_ni#?!Sib4fPB7K#EP>qz>4E=56I z{L|KHwb;y+SUIK^^A+Qg;Q;I-uRE-~Zw~!7=NJ&FQ$d$3lVva!lb#hmR`4cA6y2%m(6rI-rE}Ik#>p`LBLC#dknf9?h@?Zp$nkdX zXt6yQ(Ff^Zom4jTju5Zh|DeNZjB7FiZTK`w(JfDRA_wxMUGB=lBaUOq|x8<5bH2XfA1ihpZ6%}bJj;=DoqfPN!|Bc1cT~9XI1}ZsuIlW? z$k$KILN^Mz|Luu6c_(le85(k|1et5;mUUFxf8Z6$E7m4Ts4sy{*e;zR&(@2ntswZO z*88gdpggHHy=hXS7IbD<36i375GCp_-nc)YvH3s+-1fkzZ8A2l@vKRuu*aJ=lP?6h z)3e`2{$y%@>@T3_3y|IT=F^E@#Xh#TuTcP5Z=QkHT1LBHAiej}Pv#Qs5?uc)vZfS} zErWDV0=Rdh>dPylcC@XviOtifxIHi;xi_BczaitK!dM zfWLLw6h+|{)`Sx0HgQRAnwV8Ob<6kN&ktIaHm&=v?bxl-k)e69&Q4{`!h0#z_S37z zmQ4`n7YBOMCAY}t9_DB4_dM+s`VQojn7=IlTV_nzw=SK+d{nbPj#97V$1J?9x>)HR z#*I{ctvR;F}F<3V2@tU`JS?%d0kJsyYJm18dq+s>sdT*i0V=`7d+Ew@PWfwB> zZs?tq8T&EebpVTV3L&-R0tGlpzld*CKv$NeBj+2-MrU4III&^wHpkfpyE-pPhdV9dK`aOH zN!#PRTb3NamLJHs;7nmjb3kqO%F#(7N`w^AA-&ZZ{B~`L4gMZ{VAcd@>MY*EduP5e zcCG zZxg{O>Tgw)unIsXDy3$^8qBd^`DVXvHzxe}#@na8SsbM3orMWL7qo-_k+WlJiZraR z!xI*?#KJr&0&arX|RFuVNzF~Y+OO5~u)%D7AfPBWlq8n31AQkc#Gj#|bA ztV$-pO<*_1uA0pfJjqi!orIriWtSJVjlqQ!jGF*-*a#V9xxmdStXmnU z-5KsDou#Nn`ZaOO>sVw$NX#X2kZcQ*Xv95qp!V$T4?!Htk`D@l->^-3H}9p#mF8r; zoJBz08>E%Juv)AsRKX<74o9JoBdwR$Ib+6J!#nYiV>}+qg%|*!%e$e!HJ*`XTZH9r z*TaUq#-H;uK#VgX!neoh`=|L&1|{Ky2Xv?QgqrFvk5;6RcoauijFNpMe-uk&^D{|x z#3%=KW%F?iBT{yB4Jz*}Z-bfibI8a1+76jZ39vE4E<1ndH>)S?*_sE|%zwFzaH@6o zp=V`MF7%^^C#EhQ&P@O1ozuG<;2*fq)a%s?AX>rN1i{b%TC1=yY<~>=wu+<@7<$FD zcf4``T?}%}rY_+OKy6*mVNv{$pgUY?Y{C1C@BO+ zee8~RgzRT)jdE|rG0K;TLVLW$n=#o9AK$60!+XvrQ$63BzuJsGouG~jB3APx>PW?a zn_b?65gL1Yn`J8G$trCnoi)Nc6QK$PZ#?QJe$ljSe-B2=lU;vD-N?rp{tO<6)lkm4 z)c(DpdU&;Ay@+5rIh^{mX_ttg&upY2=Xy^@k}Js9(ufWfQOi)g^JMyp(pUJO$3L49 z9W^Tx-;rSsoy}piZo_qtEd>N4N&JYqM_2tYGuPhnd*2;XObu{4O(g9F1P$zMC`Q!Q zRuIm~UJWl?hG19*x@QDdn_FOz;wCHGW?_AD!yk%Xe*i>w2R3!#fbH9`MencLPaa*( zO5-zJ(3<53=88u?Y2L>|N<_2+C8vghu~R@h$eTD3Z+U!+YrA>1rMatlX=a(2M{-6A zmIZ3=8jB018I`_U)b4if?5h|_si9x_YX!3Y1uhg7Zh%jW3Cw6I{F2F+5O44DeOt4d zJ}&}3dRB`)1fVVt(3oqvA5qM1-QX@2Lj5@`x)IxXUxk2ghSgu1C`c8`V5!j;2+#;8 zKR7tmYb+`=dRl+t@fqQZHLN6(9iO=Ey$+~YDBzt3!_imMW+mMlfBaSOI|r+3<4#IV zwOX~4*{s;re-Puw)yISjRSlsB_>!Fl&ECM;zGV8*Y5ya#KU{OvZw>YZN*isy#Ob#F zvyEMRdqGQyQC+c|uKH}j8VAgWU#9tQBv+UVdE*-oyKz&pU&IL5EZ-ib^=gJ)D)%xI3MP$+UnRts zd6do8;Q@JtSD#=w(<{CYI@}&=rYiv5Ha@~1X2&-8IB-&*xdwe>yJcGDWI*FU!|Zr~h<&3>dD~hWv>NlkT&Wsx1V$K@U2s8U%b^vZR^R z!zUL95womhOw(Z?mc+;nSbV8*ki*FF#Qia(g;SwhyCEP4d{0gnTl|-u97S?Vxjj9n zFNfT6m`qgf2BueqpOAI?8U~wDvPDx#OVz!0wkXVbrJoC)TP08>s6&r(U_Q)1Y>LpGHS=c)#@ z5IrqNi3~yaKzTnz&5UY~y;5toyYM|W9T7Tk@JDG__V+ES=4Y8_JN1L_B?7?p%^O4g_@)(e ztMzTv-A29iY5o2^*1g9h2hWo)?u-5>9B#dG#{MtI)}z)oBvRAn}2{nCZyt8SJ zYd#IdnOAE+SFCL$^xOb%NZAu}Z)afcMC3f73@86K!dzx$k6i9oK6rpy{^TGJp9IIV zS^OP;pZ~h=+S#ac(~aHzvTU>Z8ogOgH>VYW9pzi>RGxi%?Uc-StTF*6@@IqwgX@e8oHPV8pesAFs3_v9HL)(N3m)bwrr z0}1#3Ty=0}YU@b}8JJ)`XB=&_RpnF~sbB6!ga06bD-mMRK!^OOtP_wCaVv5;dirOK z=Ok=&4RUYH_`G4Pm?~(dPr+!VVfKli0%w7_i(8c?QDJt_PTrGbsc*2CAs00lg!Is z4)7Ia2=jQ8QB`ecHVYx>J4ysqaZ zlsi3p2Wt?Itup}1EbyGWx+$JmW|aHkDWFzBd)%NLaJ3sRm^SePtEpMYnf(jdSoj)L;J;ul>zSoxyHfre#vbQDaU~?USO9N zC*dF)b=`xUx8sH`L+8Pf)%|`K+a}+A1LK?m_H%Ywg~URn3CzHpy!P~%YcW4KBg5J` zE^y3!R|Zi%t;jq;iQ4gg0U6XVU4nznx@(sW&UeMpliF9|LIQWK^An8Kz9}AAFtf%7 z_wh0BQb$>g_Rc1H+|sx8+b!);oTd1?^nnsQy^A~NkPsC21rAp`sSoH0YIa@7_D$$4 zzDU*`K>Z0q{dTdOO}5#j^qB&e3q2YuGLi$MPbMu9H7JQ}FAq8V76&yON>TYTd?O5o z)4E2>`6=e2>D})Y6kay!GA6***hZbk_RDg7&$snjeB%Fmt6+iS_j$%dz6R%)c;c&$ zAR|&{W_?R2HG5&WRh*ouR*dW#zz$o)T9EFp3pWzTszpASEq`l0;kvT?r9W(w%3A?H zDT^k~Gv8MbEJ{@*cUAyfe2@3pm`SK8eZ)?Y``sZ@7(s9(k*_O~-KE$! zU1BV={T|$r(5RWBN1dzQsy2OBis^vTLW?_)Zm1^fB-A~dZ$jDWhMnKxLVu3z_==l4 zar6nfI^W=hiN@~Kr-r(*4il2RJ2xIK^O(l-;&Y2W&9nK@g_M?!*KB9^W_HGWrW=Z- z(-k{F_1`-mF8_|;QF^-{kOppQu!&k2tWQtt%+h)m#FPN}gcV#b@@V{!iD}2Fl8o(> zd~9zg0qu>z`{54+b9@E53Z!d4SJA{NDRgoT=_eAYDRL-Q0Xa(P<8Y&jDag1J#fKi1 zaXw{QUI(?M#7G}24GLs)a@{Kpe#~bTU#z~UyJEXYcfgr*CHVK0GbJri5}K8ufy?E~;ECD~x2Y6Z)XHQ2u)9X&z+Xk0YNIa*LLl z{8Nu7yT11n{CirP?c_%eacYOMXK9uVZ2vfT#HK=EaT}WmSg2Zj>T%#Du5@xpv}O`H|v9Nu)kKN}{%_32O-1*NoQ4)bL|C%HNsjmp`F>eJgSE_|@J z*f}eN!3gD5+_@`dao;^-XjBp8H43YhJ<#M0jULhB7*wFdGYJ z4xu!MR@VZ?6Vnke`I%Qx+8Tfh-E1oEYGl8tAsFKM*vl2m{x|3b=j7nk^`IMtKkoi_ z+cY;F(IduzOMSb0>R^d_qW7ZT`})Em zK*>5Mvz1sWWA~YD(hiS4lDs&FKd-N5n&_!vaI4NMXq^kyBST35b1Y(aLnKsEHJk*> zj(YW9Deq7(h=6tJ^f}m`%i?Gi=G4HzijXs`ZJ|eJj8W0A)^75(I|QfPuoba>t<;m- zruMJRx`T({Zp)Y-*X$R?UH7@>Q?=5GVhQ|X1i>-s>Yv*BRR%!YxxZz#PCl%azMS+8 z+#CGfRM{Fkq0ZpQiA5!6&+LGUJsxTgO--ZuooSC_>Smyy7_#1GH7hOe52ZMd{ncNO z=--)W+>yPux&QM-Gd4zTCa`hwEx!R{{Y72iv9pq7bHy*A>E*xE!e#dil-LiFhK-JuHv!(HT zTI?VIw)PIc9Xj@57tf5z?QKt!d({KS_6PP#UkBZ zKlY}D{(${RIj~2I-$O0PLHknt5SM|69YiO5eN`GUsmguGwLTPt`J87bIEn*msGw>qj@u{NX1C^5c<73q{XcPGt zse)7~yOxi^DeviH*$9eVD1{MWp0L9%E&7zP5@T|QUaY~O-DmJ+LC`>HRMw89y9}Wv zlb#Z?-_&c6HC|M}bi74#O9vG({y0ClDF!sKS$}eth~#G= z2ZSfBS)WEodl8w;;6Y&(+q9dTuZBA#!P>zcu=Uf>tAXpnm9QD453k$S@T$Uw7UMz@ zjhu&!Sr2h|LC~0i+U6pwt9f0cy`AP(=S0P!aKMx4fvsE9u$1&bGhY7+kArhYE=PpL zRpe2u?1vv8LO#~m0tOmd$in4=n+#kIV(`6>ZA##Aq1-)zeOKz+4@Wva%MA54Ei4K` z#6v)`N2P2<$7B8*pN(XDaNmi0U$)rVn6m=N>3V0jMI-v$9pIY{R+j-w@3P|Uiih;V z>%ApvgP?2E_RZrfCp!~@lZxb}xh-c(sLFDm!4^Nf5LMqdO5fq-Z#q=^qax}FtYX+m zVSGBiJd|j)z8u{-oZ>4(Nl~mP6^Nw}FN=n~+&UIA-$j8*RQJHk@iSKR1m_fi;qs54 zkLuhAb=o9Y6B-QXRL7w1EN;LjyAna*Prmb?%R`JvwZr3MR{+JTuF9dzB+tR4xq)A# zc1zA4OS^wM#W%t8J0_<%uJNJ0o-!m*sH7PBo;o}aORs62X#Ca%b0wf+ zXZc)NoQGP&UQ{PhD6o-Gxxn|5_qD?b?GX{5$!>KNaw51=M5|_-j3ZOr`pQ{C-I!g1 znq39h@8xu*+lZn*!y3S6^gQP8qB zFz3y7?KxpmWryEI4gKSPP`iJ#G=s2N&vtYDP^yfJ4Vv`lTY(C$mDwC!sP0r3ZQmB+ zbh3iTy=C+7D`y^R!rn_3y2gTe3af7jUMiGbPTI|X>J?+`|6eZy6Bx+P^`!f4>v#Q( z$fs+5+qX^sUYdV7-A1CGx!0MZyt8;>_0x8-wuuILHvJ|RxyatMTQYU2b-qrCgP)5H zr)GY*vRTOw;hi9VchZdNRkJM7+Wainbayx^h&jOBJsI2??go!JX1zbR*;zibH|2Ow zu)>*+Tt5r?j6=>*5td}Vzm>nbmJ#(QiVtF~Y5M=z?%*d&Sdpr~EAv-YE^P*GlIPGh z5#0@S_y15E!5v%IET1tHqsX4G_8qX!do<#9zKaJ%9Zl7YV=TK@uhteH9I!xVtt26$ zN_6>{?yI%aP3DkkiD<9pfxM2ec>&fN{;oja?ED!TpJYq5j8n(mW{eeq&C05Jz*JBz#Ijnk8jH0lhmX3JeZ>507*$a;^g7;A1& zQiYVjwmi0cH0-DLySZ*2j~lQWiPh^L3m=tgAWR<)`V2mcf*(%Qx_enW;XCPzM@m$m zP_{TOB(Q4wdUq93$=J6$XRVe_ z`ciMm97`0=7Fc(U%@`6xW3!0j$0NCQw@XUB-HX@Vfliat%iuR#3}`t3H3TaZK63Ei z$ILi#RS&hx68OX$?>R^owx26m4zD$poxjt{GZE!8T!JZ({PCDoD5t0TS!@C6k;Ne0 zfGNg5`{WFHxI2Mfsn)S+;q_nvt6{vcpuw3Y#l>`%zXDE0w4AY zpG{*cInjBK^mWHSKpd^?nhXLA*-9+V>YLpeKs)=IMNiQ9?{q4Z`>R zR!8Q{t1ZxNHB~pmB}MEIZ8ci-nmCyt+BCJXF!RFHv&g!5YUyA{-`d?X4(aFqQ-wUX zvJ4>iTsw%uQfnDfXvj1=(3YDvJ)8agIYJRf{Z5YuD?*N)stQ&rOr~O#xrf!NdW>I(Q>oQYF714UqQU|T4z12)jnTZq&YP}jre04x;^Xl19qsKw-k$9mOSP~=FA`u zpsL+I1n3j3&k@k$X3Nc)&iklwG~7z{?ud4~wuMuXEwIkd)m3pt{_bbiHRiTu_1w@E zQrgQg<<;AA;#CT)xIw_eD$~1XUnzI>O30T!7NsdA33l2wHj9ySn_qT}Tolg`l1Zdl zK4|42>ulA}wWZ*|Tx=28+II6-#7Lk)Sf(b-dPw=0H%!$FR~YSckgqS`lCLyp1YE<= z5MJ$H9sLJDRQRifbJtZ~Uaj>B3&KBsuNtpsInp+!PT0uU=9q8CV)1zOKY#NZqPFtl zhI`jtx|1A;IuRy(_=Yvc#Ocz2P7bi;3|qS64C-VT2Qc+(_Kc%-MEI!+ydIOGx{~SK zZ*00bUzMu6?@3YD6`G%T>s$HGEFju9S&F060{#kE3#*yX%-6QuTsU13%_gwOzfH{D z7?p)CqS-uqPu*X$|6=L82OhsH44(?xY}&R4DjiDxl}kjw$TXpno3*oJ#1s(aTv%P1 zE`^Ow^_7XTV<_pH!@?f8Msv@#%(L0B{Fk5#o^f;T5p~>PYN8=8#-*P8pkZ|!TkZVf zW3dcpQ%f_oJC0n%DV**<<1_z-y0MEbk*;-5@$If_n9!+<%19tl3uPUBbN9Z5WLC9- z^F6ai@#mn_k|xyDSOC>&Q<39jgnjkV&6UzXO&?!seGO6<0E=9QtGd)gZ$&L z!G)r338huy&8AeYxmm}lG5j`?G>K~tm2FF3V6#XM61W!)==r>5@oFnA9`#_c$3Adl zBBZIun(3J!FQw0n9jgutqQ^bxs@xaRA_fC%rSQt7LzzlhOZuPW8BA!V{o-l_Vo_uY z(Zb4E7T55Xn{7#Yz!bW?4rkhxEI{|HnmSGzAaqJHbILOHC^D#_ui1Ta5aFek9IZCd z?Az~Az3D7Ip)(fgxtl~j0(>rwuLND%y{UbC!AVp3LChD&F)Kt#jQ`e0E$>R%OR!bg z?@d4k@NntRN9cDodWc4OoyG7i;@pMC9tvObEg_3jr+B{pi`0};O3to&OI5_j@I4%`J>+UE5p4l)X}NfZ3Dwp;$yVU)n9FmPa_C6-b9L} zKXFr-*WSO$yS9lWEn!_3!)Ecd5``4xBpZvFzpxq)k5JvI)WDf`aQ|6ka$Wvo;KdXf z{VurzvO0zPkCca^Q`GV7LUmc*%I6 z#AEM1e?6mdJh8Kif0;KZxiCII;M(POlfFsIn>pOF;;z+0|a!aa%HxPQrC{ThRu* z$m2uK&XvLEiQC@C&{AT%Z$%*KrFli5rD#Wx+d-f!c)a?c5eAB>A0Sq(L-S(L1LBE6 zM9!)?5NdD>agM4}+F6yRK=L_m!_p^=tCz(Fz{KWhH-=F$QU)Znjw~nxH}3)P2{_)X zitp|<*&^Afy#?BQp0m_DDpE(FdvP#c7;V>0sGdR;=}{4lU-Yn|VUywE(bH*5jfDUXpC zmZOQlfdBT#TINe9lPTm04tuEWOqc0CYmOty;B^r7+S|X`CdcaG`0A2t<~%iZNCzM@ z9rCY-@A<;^0SJ-Sv@S|H^zW@{u@ndH%q2ol5Y-U%ao%MGZMW1X%1Ra$*V)iWv{1Lr&`7%G)Nh6$Qh zJwW?vn-?X(jCLb1aC6Ta=jD%WhJKF;j02JuoyV@alL%3b$c|2nXXr0wuS*IFIqR?P zKH^_vaz@|c?q_k&$vewSyMi7nEoL-+tJZME zJ+kTFQQvc#z=X=l-pf zq%__b5wJ(QqZ-pRRw}oR(bw0lJ`kmtJwAR8s&F!XLCgN8K_Ug7t{rp>Rc8zveIg0A zg-*O^zDp~f`F?~}tW|NS-V>@aL5!03$G5(u9rT&)?f~f21UZxC@Uld-Qpanq{lel% zJSOJ{nHTj<R`>ijYebfXy^<|o<~gb*fvE3ma)kdW zLT-XuNrKbI;To{0{cBr4P&m-{wbhF1(qi?)Nl&vXX2*M95WM>_?FwS~bUH~xJHO|2 z(VML4l;1MRD!70Gg3*a4+vFZDwm%0n*QemZ@K|7Ho0iS=b6N%>@n7JlsR?3w@WCRp zPo(mO&7MW$+)P(>wA%04uT3_8UEG{?!rGQ#&=~!CUTuF}tyo??Fia6luw^HG+&B2G zJge>>MW7rKt*7`l4=(-Xhv7Mq*E*B)vqfpe8CRFveJYo@s%FbigA? zRZb2@Z#4S&)v-^R)CXyfh3G42|9mwQ>ErJ-`%UBKZ%pd+T_?keZ2WI+^ zsV+P;Wj+@o%85bko|C~oH@81gtS^{$rMf!JMC#8P?-95y?u;?p1HdM7LylgRhc&ob zTN~UUAr@<2Bv>WjpRZOlo)hLlakGWioC~@(i#uRnIA48wdf0Yjf#x?K&(nF>W}c1x?IU+nt8)z z7tX(+6(oc;&KhCY;=;ThFF!~dwSh$hR2DvGe=98qKIYZXY0y2$=U`vEy*O~^K=pu^ z1-K9%B(>IHtEA~*-ZNv8whW+VTkIR$&_6KEMav%>IGhgEq(0=o>6>hYibWoXvz*BG z*7Y-r{SoOgjQu3?J-VK3`$tHPC7&!YdQ8Sl~xu_WdS#y1X&nEjJn_gm{pTy*U? zXhEo!euLTrvD!am&~8N*R{-FZmM4^miInsR9S=ES+pK6(TyVV4C{br#B&uwixrAh% zeT!v_t<7q!UPT|=0J`6vY!+ZH3<0!CjUNQtfJK-(E<3|1Tksx{vV)7!oyfu_m)@-E^A-w9%y4ORh3*FjJ659v$8{hmid4UGai%ej--I%fY zVIq12SPQ?Y>msFOnj2IX_xMFV>aNPMNO!5KOAjkpcO%ud8)6pa82DpD!dxNihXgYn zZ>^!Tq!+Eb^thwoDW>PL*F3WUcxDMBYz;xU%O)R0gzeiUA;5G2i3ro>`$F^2!eSq? zBNIO<1pG4L)>HN<(vS~Ts@BUKjj)M6?n>X4D|zfx^ycbZ@5bVpP*uIiW$rRi_HYV&!Oqv8m%6zL6XI+B#}tH0QM~ zzxfM=9$TrxNOJ&2x_`MiqsXJm$~R}{a~&~#U34zBp98D_Gm9e ztIEMsypp0#%!j)HzPx(*9)mxa6sJWa;f*4r*5o_;MpuGWBX@_i`PV;QgQUmTcIo)* z;!JHFOItOiU=qZMY$(w%J>ZU_QcmX)_xS*ldy6&jwoDEeYrZDY*k4!t1tmgnVpTFe zItZ+o@{c|-)^zs~aA5=@bnxfsr@l}}0TY_w%)hUlpLzpSEeM0D^FW?3t=c1eEEj-L z%ioR?@wHjJZ?wT1WpN4eAz{UN-{(_}n2e%IXZ^~gjw=cApIJ{Z8Icjn)%|Kll(?`H zi3{v1c^X&P(luR%C4z#QM^Qb0=XeIXHW_?_g~#5b~{2ngGxt5QxHjx=NGei(>M@YxFKXB4n$}&BEJ!HCCnQ$+}MA&QT z0lV*T#&5m>M0f<*RJ=sC{C2M6Sp6E|MUOuJ%_wpdSs4_q@#J!7vpmQpL9Og{^sZw= zrAC(HlZ)D5;-IS6_FbjMI_!gh@sn(V%YJ8RjQ9zB=-wRUIaHNDD5$}`~~M$F29Jt<;_gIH3O?VsGIux~>g zlz8FJKzhSWldHFf#~~Sl^k}GBc^~jB74_oJ0xR`uTbAW|fwRSbDrH#@2yZ_F@EX5y zYu?mE>Mb{J)Tv)?;mo}fXo9yp-wD`wO^brO#zKzTt=ixxe%~0Vt~u+_aAJj4A}&22 znUOg}&+R!`s!H%rKpFvccXSsmbxG)|tp8xm>`fGU+h}^(`@lOOGS{c-R+i zD2<8=pWNpCn)Q&Q=Y{}j@sWCmIj(K|xxf*x-UWWW5z|?sO}9>w{`G$8-NdDwFTaws zk&EGz?c5W8!ft9;e#1AH(ZY=bj~(?o+O!1rO%0n9V>||Py+(qV0kT+d117*^m1r(# z3fvve{(uevjT-;f{OsDcTwVgF-{q^#&qXriZL`!zc}hX6x4U8x zy5l2gzk){r8@V`c3ZaUVqvLCaYLK%`%Xi)X{@frVex`B9;nhdZd5Mms=iL_^B5R_< zF!{>ca>-Yek_#{`HEr<;rmlL!iZxwE$tiENgbw0F-*6e(nHUpwP>bKoJ=u;2=_9uw~AomlUGRD*DpqD5mGZLUXK%RG|n6H;;w|Ls*EskIGQ_(dP7%{ zi!f@wDw0{3-7He`6EdX5XvDmTk?HZDAypNa6s9lx_IU*NGB5xeYkDk&kKr$S>b9z&kI&XTq%^aFdPt!EZPsC>oQm6fPnp9};Y-*T)K4GAvq) zhVj45E%BKj)=DSk+`Jnd)94hY4}iI!<|5sQmB{c%A!% zGNJ_{*#0Lnd^&ROMvHNHy(vJe-i(}gPW~Z%+|@}o@;dG^){JL0ZLhE-bn}tgHGY)s z59NBjkxyouOs2I3G5t_ylXz~u49#CFZ0+E1$}QNN;M%`ORQ|sEbi$ArtK7r=qo5e9Tu_};JC;3F;PRs5HA+c)jQrdL&y;bc zy%CU|k0$*%Dr~of8%4803HjT=<24N zgYtmk-1_C`b|!uN#iwxSh-dRk9yVL~x(xwcpIUR_vv%zJ&zbu+`c~~uf&012BUe2%1Hwg;4PAysK84L2 z1`@)x1;ub9MO6dM$Kkm*0c*E0F5?A9y!^g4WAVF8C?40roufOcT~Vs@&kPqnsw@V( zJc}%phrl6qSsZ~(XjvtT5l#oZ(__@Z$`1PmNP5n}pl>lUegaok<`o|g`V^?gy6k&U z-HBH?1(3h~gju~XRDN?uSmj1Ddv9v<LRH-~~!Dfrkkglv`GHB5nX+w-v9dF7zJfVhWtq_Oq(z zBqbN^WLmUja9mM-d>PGM**lBT<$q<9Z>0OaZio{G;WWwQz~8JsLjN6NfC7^-5SDTw$Cc{)5}ATjr1ox^Br<2!aly&l zv8k(3lwk3YkaS8MixT&|bJZ;lFIyN8gCD=m(~0;8?FdVY&dzLm}qX@hZ;hWUIUGlgxTU&%leQY3UO||G;yC;h_YA2Tqm2eUnfsYAZL2UdB?;eP7 zs3MKwi#L+%uIg-ewvw4+;_gFnhhYP>CT72b$(n*EMrYeKl2veJvAiQ@;wA>feSprJ zhvkoC2oai1>=5YUpwAmEvkwOTarnl{sV+|81rf8Jjh;d|Cc6UoPGvJ29C9SFm5$EX8@z`ywc$%}D$MqAPM9nPwX z{^AS5|MgbutHPP|%d62XX|K`ecCg_9 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/producers-consumers.png b/Greenwich.SR5/images/producers-consumers.png new file mode 100644 index 0000000000000000000000000000000000000000..9990897dd2d428d7f2b80126e5b29841040e3237 GIT binary patch literal 15947 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/pws.png b/Greenwich.SR5/images/pws.png new file mode 100644 index 0000000000000000000000000000000000000000..e586a00d2627aa128676c1ed19584dd2c3464ccd GIT binary patch literal 13784 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/rabbit-binder.png b/Greenwich.SR5/images/rabbit-binder.png new file mode 100644 index 0000000000000000000000000000000000000000..aabf698b3a9873395764d1d974996a22436188a9 GIT binary patch literal 12440 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/redis-binder.png b/Greenwich.SR5/images/redis-binder.png new file mode 100644 index 0000000000000000000000000000000000000000..1832bd2e31af35537088abd41f7092e5c63e3b66 GIT binary patch literal 13731 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/registration.png b/Greenwich.SR5/images/registration.png new file mode 100644 index 0000000000000000000000000000000000000000..d2c044c1e62a9536f94362c8dc7ad89f0231f9b4 GIT binary patch literal 22405 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/spring-cloud-launcher-eureka-dashboard.png b/Greenwich.SR5/images/spring-cloud-launcher-eureka-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..dbd12c5922709d884f478126ef5b1dadbae47595 GIT binary patch literal 112368 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≶x*ivGO1yH-x`*KZX+-8Ie!OIIj>qguq+h!Nper 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 J%Q~loCIEX57Ty2= literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/spring-cloud-launcher-log.png b/Greenwich.SR5/images/spring-cloud-launcher-log.png new file mode 100644 index 0000000000000000000000000000000000000000..916ce243deec4ec1edba6a5b727509abd7690572 GIT binary patch literal 281927 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
    tbB_)9^LN--yhYB*qK~B(7 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%0Ab2P+Qp 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 z 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

    u%s;o0-0-EJ|w=C^MR13RPMyBhS%vgC;C0%^bw~ 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

    qxAnsgKEX-u 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�^QVcDRg&rM1jUo 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

    VqB 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/tip.png b/Greenwich.SR5/images/tip.png new file mode 100644 index 0000000000000000000000000000000000000000..6530abb4b5a5d68e3b1c98de512d264c1f8ba883 GIT binary patch literal 931 zcmV;U16=%xP)bSEogsT%F|x0xuT7xdRv7>Rev?4wv{qrDN}+xS$8V5!!ga&#Y1*BgqL?&c}jPc zG_JlfMSD5I%DQQcHXTbGWQtKpeL6yAB|UI5CQ=~#`}=c}Um;E%R)9u^qI0>&GHQ-g zOm;DCkym+{WF$}@UWrV1mtnTPtu!WtY$r7BOpo|N_#mqWGhK#KR0MD7eW*yPaY&xBTRfcG-E5p&`2dq z875XFdy+3GStd(wD_Mg`dys8Xd_houJju_&*4)mZt2Tk1H)DTRJY^_lf>>*ZU2Th5 zWQ3Ly{;kf91GM2s4Vfv8a-fcsXpb+4t> zmM%11X*>M&PQZNVdARf4d*2x!aq1>jOzQ?>>R)(Ok;sOJ)7jfk$Fdif23? z-}3V78&9qod*O;uGk%fEW^;|k`Lo>bOq2iF72o-IGb2gTw+4B~#iYz(oL}sS7|$R2 zDGfrR{|@~AQJ&v(#4u|ZtJP}t520N48P!$8U;|Vfuq=8>E$w`o2Jf`%eqhqbr%IH1zV?O3uDWqKZId-wMQ*MFefpD5X*w@ zok{kNA?%%$F{M!OUcE^^x{~(wkHK|_*9Yg`KNS88FaVH_sda1Xfs6nE002ovPDHLk FV1jwin)(0$ literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/trace-id.png b/Greenwich.SR5/images/trace-id.png new file mode 100644 index 0000000000000000000000000000000000000000..5e54aea245c6a1a79ddac9a6ad7ec74f288d4320 GIT binary patch literal 86175 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 a$T;jBs7t&XW$?g{f~+bSY;F3U`2PTx{>-xg literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/warning.png b/Greenwich.SR5/images/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..0d5b5244605adbb7ab05a1549746a9c35490f95b GIT binary patch literal 2130 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͕Y$7MUW4?O++6bLy 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%0A_}B|`pUb7QeGd9E@=YiLA6`NCA*_%;Y7Ezc3;oT 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%0DulG6m1i{s!ruAXx}z+ylAD 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 TuhmZhKPpO^4>9*G0xA9vg3rwP literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/zipkin-trace-screenshot.png b/Greenwich.SR5/images/zipkin-trace-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..5e8770abe26cc2fcb74e0d379456deb55b3c24a7 GIT binary patch literal 169485 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 gHa5D?`^f0^D)(c(OizMN8C9Rw(yZRSLjsH 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%0AKSB|mV0)vF_p6XU3b75F9R_yzJ&&44-dqb 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/images/zipkin-traces.png b/Greenwich.SR5/images/zipkin-traces.png new file mode 100644 index 0000000000000000000000000000000000000000..9af0f64d2689171c6e65fedf910f568080ad793f GIT binary patch literal 152670 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 PAnna(v@CAhd?0I&>^4#QbP$K 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/multi/css/highlight.css b/Greenwich.SR5/multi/css/highlight.css new file mode 100644 index 00000000..3850f8b9 --- /dev/null +++ b/Greenwich.SR5/multi/css/highlight.css @@ -0,0 +1,35 @@ +/* + code highlight CSS resemblign the Eclipse IDE default color schema + @author Costin Leau +*/ + +.hl-keyword { + color: #7F0055; + font-weight: bold; +} + +.hl-comment { + color: #3F5F5F; + font-style: italic; +} + +.hl-multiline-comment { + color: #3F5FBF; + font-style: italic; +} + +.hl-tag { + color: #3F7F7F; +} + +.hl-attribute { + color: #7F007F; +} + +.hl-value { + color: #2A00FF; +} + +.hl-string { + color: #2A00FF; +} \ No newline at end of file diff --git a/Greenwich.SR5/multi/css/manual-multipage.css b/Greenwich.SR5/multi/css/manual-multipage.css new file mode 100644 index 00000000..b790654b --- /dev/null +++ b/Greenwich.SR5/multi/css/manual-multipage.css @@ -0,0 +1,9 @@ +@IMPORT url("manual.css"); + +body.firstpage { + background: url("../images/background.png") no-repeat center top; +} + +div.part h1 { + border-top: none; +} diff --git a/Greenwich.SR5/multi/css/manual-singlepage.css b/Greenwich.SR5/multi/css/manual-singlepage.css new file mode 100644 index 00000000..303192a8 --- /dev/null +++ b/Greenwich.SR5/multi/css/manual-singlepage.css @@ -0,0 +1,6 @@ +@IMPORT url("manual.css"); + +body { + background: url("../images/background.png") no-repeat center top; +} + diff --git a/Greenwich.SR5/multi/css/manual.css b/Greenwich.SR5/multi/css/manual.css new file mode 100644 index 00000000..20cf07da --- /dev/null +++ b/Greenwich.SR5/multi/css/manual.css @@ -0,0 +1,342 @@ +@IMPORT url("highlight.css"); + +html { + padding: 0pt; + margin: 0pt; +} + +body { + color: #333333; + margin: 15px 30px; + font-family: Helvetica, Arial, Freesans, Clean, Sans-serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +code { + font-size: 16px; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +:not(a) > code { + color: #6D180B; +} + +:not(pre) > code { + background-color: #F2F2F2; + border: 1px solid #CCCCCC; + border-radius: 4px; + padding: 1px 3px 0; + text-shadow: none; + white-space: nowrap; +} + +body > *:first-child { + margin-top: 0 !important; +} + +div { + margin: 0pt; +} + +hr { + border: 1px solid #CCCCCC; + background: #CCCCCC; +} + +h1, h2, h3, h4, h5, h6 { + color: #000000; + cursor: text; + font-weight: bold; + margin: 30px 0 10px; + padding: 0; +} + +h1, h2, h3 { + margin: 40px 0 10px; +} + +h1 { + margin: 70px 0 30px; + padding-top: 20px; +} + +div.part h1 { + border-top: 1px dotted #CCCCCC; +} + +h1, h1 code { + font-size: 32px; +} + +h2, h2 code { + font-size: 24px; +} + +h3, h3 code { + font-size: 20px; +} + +h4, h1 code, h5, h5 code, h6, h6 code { + font-size: 18px; +} + +div.book, div.chapter, div.appendix, div.part, div.preface { + min-width: 300px; + max-width: 1200px; + margin: 0 auto; +} + +p.releaseinfo { + font-weight: bold; + margin-bottom: 40px; + margin-top: 40px; +} + +div.authorgroup { + line-height: 1; +} + +p.copyright { + line-height: 1; + margin-bottom: -5px; +} + +.legalnotice p { + font-style: italic; + font-size: 14px; + line-height: 1; +} + +div.titlepage + p, div.titlepage + p { + margin-top: 0; +} + +pre { + line-height: 1.0; + color: black; +} + +a { + color: #4183C4; + text-decoration: none; +} + +p { + margin: 15px 0; + text-align: left; +} + +ul, ol { + padding-left: 30px; +} + +li p { + margin: 0; +} + +div.table { + margin: 1em; + padding: 0.5em; + text-align: center; +} + +div.table table, div.informaltable table { + display: table; + width: 100%; +} + +div.table td { + padding-left: 7px; + padding-right: 7px; +} + +.sidebar { + line-height: 1.4; + padding: 0 20px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; +} + +.sidebar p.title { + color: #6D180B; +} + +pre.programlisting, pre.screen { + font-size: 15px; + padding: 6px 10px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; + clear: both; + overflow: auto; + line-height: 1.4; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +table { + border-collapse: collapse; + border-spacing: 0; + border: 1px solid #DDDDDD !important; + border-radius: 4px !important; + border-collapse: separate !important; + line-height: 1.6; +} + +table thead { + background: #F5F5F5; +} + +table tr { + border: none; + border-bottom: none; +} + +table th { + font-weight: bold; +} + +table th, table td { + border: none !important; + padding: 6px 13px; +} + +table tr:nth-child(2n) { + background-color: #F8F8F8; +} + +td p { + margin: 0 0 15px 0; +} + +div.table-contents td p { + margin: 0; +} + +div.important *, div.note *, div.tip *, div.warning *, div.navheader *, div.navfooter *, div.calloutlist * { + border: none !important; + background: none !important; + margin: 0; +} + +div.important p, div.note p, div.tip p, div.warning p { + color: #6F6F6F; + line-height: 1.6; +} + +div.important code, div.note code, div.tip code, div.warning code { + background-color: #F2F2F2 !important; + border: 1px solid #CCCCCC !important; + border-radius: 4px !important; + padding: 1px 3px 0 !important; + text-shadow: none !important; + white-space: nowrap !important; +} + +.note th, .tip th, .warning th { + display: none; +} + +.note tr:first-child td, .tip tr:first-child td, .warning tr:first-child td { + border-right: 1px solid #CCCCCC !important; + padding-top: 10px; +} + +div.calloutlist p, div.calloutlist td { + padding: 0; + margin: 0; +} + +div.calloutlist > table > tbody > tr > td:first-child { + padding-left: 10px; + width: 30px !important; +} + +div.important, div.note, div.tip, div.warning { + margin-left: 0px !important; + margin-right: 20px !important; + margin-top: 20px; + margin-bottom: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +div.toc { + line-height: 1.2; +} + +dl, dt { + margin-top: 1px; + margin-bottom: 0; +} + +div.toc > dl > dt { + font-size: 32px; + font-weight: bold; + margin: 30px 0 10px 0; + display: block; +} + +div.toc > dl > dd > dl > dt { + font-size: 24px; + font-weight: bold; + margin: 20px 0 10px 0; + display: block; +} + +div.toc > dl > dd > dl > dd > dl > dt { + font-weight: bold; + font-size: 20px; + margin: 10px 0 0 0; +} + +tbody.footnotes * { + border: none !important; +} + +div.footnote p { + margin: 0; + line-height: 1; +} + +div.footnote p sup { + margin-right: 6px; + vertical-align: middle; +} + +div.navheader { + border-bottom: 1px solid #CCCCCC; +} + +div.navfooter { + border-top: 1px solid #CCCCCC; +} + +.title { + margin-left: -1em; + padding-left: 1em; +} + +.title > a { + position: absolute; + visibility: hidden; + display: block; + font-size: 0.85em; + margin-top: 0.05em; + margin-left: -1em; + vertical-align: text-top; + color: black; +} + +.title > a:before { + content: "\00A7"; +} + +.title:hover > a, .title > a:hover, .title:hover > a:hover { + visibility: visible; +} + +.title:focus > a, .title > a:focus, .title:focus > a:focus { + outline: 0; +} diff --git a/Greenwich.SR5/multi/images/background.png b/Greenwich.SR5/multi/images/background.png new file mode 100644 index 0000000000000000000000000000000000000000..15dca6fbe2669fae3609605e49c69cc414f1b6ed GIT binary patch literal 18255 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 EKU&lfD*ylh literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/multi/images/callouts/1.png b/Greenwich.SR5/multi/images/callouts/1.png new file mode 100644 index 0000000000000000000000000000000000000000..7d473430b7bec514f7de12f5769fe7c5859e8c5d GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQC}X^4DKU-G|w_t}fLBA)Suv#nrW z!^h2QnY_`l!BOq-UXEX{m2up>JTQkX)2m zTvF+fTUlI^nXH#utd~++ke^qgmzgTe~DWM4ffP81J literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/multi/images/callouts/2.png b/Greenwich.SR5/multi/images/callouts/2.png new file mode 100644 index 0000000000000000000000000000000000000000..5d09341b2f6d2ea2d1d5dad5d980f14b4b05dfd2 GIT binary patch literal 353 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 literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/multi/images/logo.png b/Greenwich.SR5/multi/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ade2ce6ed9d9e9f2f4d9c5729a252ee618a0a5a7 GIT binary patch literal 4387 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 FV1jwin)(0$ literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/multi/images/warning.png b/Greenwich.SR5/multi/images/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..0d5b5244605adbb7ab05a1549746a9c35490f95b GIT binary patch literal 2130 zcmbVNYg7|w8V(4q($)50y>JmGlLW#g$xLn}Vd2Si)}#|ptPAQp3Bp-3!-lL0;i^LY?;i#f0m5s49g z3h?3rDQg~EDPlm?FKkgKIcWEK-3X6YU0uzs7H|nq84s39r2!5;pF?SI$QqXy^Ko1x zV~zpENvp@<_Bsd`5MabC#3rvCq&$5dg43yGR zAdy0-rWjC#a1N_+kzUMY#pmogD7!DP(9dEKr3c5ngvUq_6>}Y+w-a81v=eSXnIi_+ zI?U>D1q2C!0zHox#XXKH+@|&rPT*OF5yvY$5J|)WwLqnU)c-5;=UChSlQkaY3@^|g z|J5#YBB}=i+n3Ex9bS$P?xJSKLk)-HHII@;3qG#TG^!+aj>0QkO~7kB0+|yM;YrGB zF>Ge@isQ#73%Tp#k_(tInh3U$uJWY-+DND*o}L-CB5g@#9YWWw~pxZ!Obm>yN#ZHFvL0zA3vNCTJ=t?)~`2O1M{L6un=ml<2x zQEYfifmA?Pz0tqRs^2Utt1pU0BQB1eIY0U_I}c1Y#PP7Ci5s7#IJn}xK{G+{_v^Z+1V#$Erodv>d}ew>RP0wzw+GWkGCBlS1Oj5Z!5NK zUuSR6>pa}RSEZ;qE_w1l#`hQX4E67U6NeP zHZ`T&wj0_H)t|Y1thUqnhub?%e)Y07;a^Tq%FO&iQkR&`qG!dh*2WfW(HuRQj*_DI zeCD1bUA|tMwjOb8E|FRIn$pzEU!2j_N}1Z2ig)vbr5xA0rjC70cmI6*%Uf->hWzaZ z{33HABO5q)vRhzDly2l691@+q3O{}NbSzx+I*k@|L4&3leK#$>u+T#TvTELfKb|IH zc23atf3vmfCa0&TWY@iuoA_Pm<0~ttEC*ut`isb^jXS>X^Sp=l?RXN_OJ*&usGXg$ zTlTv;Ch^vhLDf&+62jl64*&D+=+`sN_dap_1W%p|KQVYIdgPL5uRE9(c4On)DXndL zLNq?%!-u*zmMxyYc4m4Nr>>=A=s}PkJkqr&E^b9>5g+}c1%X}3|WKcg&spcJQ)05zI<<5LTBhNKRg$XRTi2{j)yl_ zj4~kKOvr+m(;vw~9zVh(zC-y9jqQnguz5r7FRq^MyKuXAENp(TAzUVtg&Ts+B_s7) zGn+fN0sG>9GjsPLKTRrvCd`71IZulJ1_1jO9KWbD&_@2UC+LTNpxxrdz#E|j|2nA9 zzB!UTyfAEJ&#%$pQ>QX#8vk@_a5iqukMF;8`wRKe{BI}5$H%OROs4J1#j)|?p|YSh z_SpR^e`VE#F52;WL{!+L(yZLRh40*KS;@box;9(-tE)`mcVp27O*>Z{_Lb*5T3cJA yr~0nPHtg2+UHi&&$8ha;`+hiaUmw&!n@8(?5PqF(KE5>Ym)EG)p&uyBP5%a8^# + + 24. A Brief History of Spring’s Data Integration Journey

    24. A Brief History of Spring’s Data Integration Journey

    Spring’s journey on Data Integration started with Spring Integration. With its programming model, it provided a consistent developer experience to build applications that can embrace Enterprise Integration Patterns to connect with external systems such as, databases, message brokers, and among others.

    Fast forward to the cloud-era, where microservices have become prominent in the enterprise setting. Spring Boot transformed the way how developers built Applications. With Spring’s programming model and the runtime responsibilities handled by Spring Boot, it became seamless to develop stand-alone, production-grade Spring-based microservices.

    To extend this to Data Integration workloads, Spring Integration and Spring Boot were put together into a new project. Spring Cloud Stream was born.

    With Spring Cloud Stream, developers can: +* Build, test, iterate, and deploy data-centric applications in isolation. +* Apply modern microservices architecture patterns, including composition through messaging. +* Decouple application responsibilities with event-centric thinking. An event can represent something that has happened in time, to which the downstream consumer applications can react without knowing where it originated or the producer’s identity. +* Port the business logic onto message brokers (such as RabbitMQ, Apache Kafka, Amazon Kinesis). +* Interoperate between channel-based and non-channel-based application binding scenarios to support stateless and stateful computations by using Project Reactor’s Flux and Kafka Streams APIs. +* Rely on the framework’s automatic content-type support for common use-cases. Extending to different data conversion types is possible.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__actuator_api.html b/Greenwich.SR5/multi/multi__actuator_api.html new file mode 100644 index 00000000..8ed0daf1 --- /dev/null +++ b/Greenwich.SR5/multi/multi__actuator_api.html @@ -0,0 +1,59 @@ + + + 122. Actuator API

    122. Actuator API

    The /gateway actuator endpoint allows to monitor and interact with a Spring Cloud Gateway application. To be remotely accessible, the endpoint has to be enabled and exposed via HTTP or JMX in the application properties.

    application.properties.  +

    management.endpoint.gateway.enabled=true # default value
    +management.endpoints.web.exposure.include=gateway

    +

    122.1 Verbose Actuator Format

    A new, more verbose format has been added to Gateway. This adds more detail to each route allowing to view the predicates and filters associated to each route along with any configuration that is available.

    /actuator/gateway/routes

    [
    +  {
    +    "predicate": "(Hosts: [**.addrequestheader.org] && Paths: [/headers], match trailing slash: true)",
    +    "route_id": "add_request_header_test",
    +    "filters": [
    +      "[[AddResponseHeader X-Response-Default-Foo = 'Default-Bar'], order = 1]",
    +      "[[AddRequestHeader X-Request-Foo = 'Bar'], order = 1]",
    +      "[[PrefixPath prefix = '/httpbin'], order = 2]"
    +    ],
    +    "uri": "lb://testservice",
    +    "order": 0
    +  }
    +]

    To enable this feature, set the following property:

    application.properties.  +

    spring.cloud.gateway.actuator.verbose.enabled=true

    +

    This will default to true in a future release.

    122.2 Retrieving route filters

    122.2.1 Global Filters

    To retrieve the global filters applied to all routes, make a GET request to /actuator/gateway/globalfilters. The resulting response is similar to the following:

    {
    +  "org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5": 10100,
    +  "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4f6fd101": 10000,
    +  "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@32d22650": -1,
    +  "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@106459d9": 2147483647,
    +  "org.springframework.cloud.gateway.filter.NettyRoutingFilter@1fbd5e0": 2147483647,
    +  "org.springframework.cloud.gateway.filter.ForwardPathFilter@33a71d23": 0,
    +  "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@135064ea": 2147483637,
    +  "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@23c05889": 2147483646
    +}

    The response contains details of the global filters in place. For each global filter is provided the string representation of the filter object (e.g., org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5) and the corresponding order in the filter chain.

    122.2.2 Route Filters

    To retrieve the GatewayFilter factories applied to routes, make a GET request to /actuator/gateway/routefilters. The resulting response is similar to the following:

    {
    +  "[AddRequestHeaderGatewayFilterFactory@570ed9c configClass = AbstractNameValueGatewayFilterFactory.NameValueConfig]": null,
    +  "[SecureHeadersGatewayFilterFactory@fceab5d configClass = Object]": null,
    +  "[SaveSessionGatewayFilterFactory@4449b273 configClass = Object]": null
    +}

    The response contains details of the GatewayFilter factories applied to any particular route. For each factory is provided the string representation of the corresponding object (e.g., [SecureHeadersGatewayFilterFactory@fceab5d configClass = Object]). Note that the null value is due to an incomplete implementation of the endpoint controller, for that it tries to set the order of the object in the filter chain, which does not apply to a GatewayFilter factory object.

    122.3 Refreshing the route cache

    To clear the routes cache, make a POST request to /actuator/gateway/refresh. The request returns a 200 without response body.

    122.4 Retrieving the routes defined in the gateway

    To retrieve the routes defined in the gateway, make a GET request to /actuator/gateway/routes. The resulting response is similar to the following:

    [{
    +  "route_id": "first_route",
    +  "route_object": {
    +    "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@1e9d7e7d",
    +    "filters": [
    +      "OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory$$Lambda$436/674480275@6631ef72, order=0}"
    +    ]
    +  },
    +  "order": 0
    +},
    +{
    +  "route_id": "second_route",
    +  "route_object": {
    +    "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@cd8d298",
    +    "filters": []
    +  },
    +  "order": 0
    +}]

    The response contains details of all the routes defined in the gateway. The following table describes the structure of each element (i.e., a route) of the response.

    PathTypeDescription

    route_id

    String

    The route id.

    route_object.predicate

    Object

    The route predicate.

    route_object.filters

    Array

    The GatewayFilter factories applied to the route.

    order

    Number

    The route order.

    122.5 Retrieving information about a particular route

    To retrieve information about a single route, make a GET request to /actuator/gateway/routes/{id} (e.g., /actuator/gateway/routes/first_route). The resulting response is similar to the following:

    {
    +  "id": "first_route",
    +  "predicates": [{
    +    "name": "Path",
    +    "args": {"_genkey_0":"/first"}
    +  }],
    +  "filters": [],
    +  "uri": "https://www.uri-destination.org",
    +  "order": 0
    +}]

    The following table describes the structure of the response.

    PathTypeDescription

    id

    String

    The route id.

    predicates

    Array

    The collection of route predicates. Each item defines the name and the arguments of a given predicate.

    filters

    Array

    The collection of filters applied to the route.

    uri

    String

    The destination URI of the route.

    order

    Number

    The route order.

    122.6 Creating and deleting a particular route

    To create a route, make a POST request to /gateway/routes/{id_route_to_create} with a JSON body that specifies the fields of the route (see the previous subsection).

    To delete a route, make a DELETE request to /gateway/routes/{id_route_to_delete}.

    122.7 Recap: list of all endpoints

    The table below summarises the Spring Cloud Gateway actuator endpoints. Note that each endpoint has /actuator/gateway as the base-path.

    IDHTTP MethodDescription

    globalfilters

    GET

    Displays the list of global filters applied to the routes.

    routefilters

    GET

    Displays the list of GatewayFilter factories applied to a particular route.

    refresh

    POST

    Clears the routes cache.

    routes

    GET

    Displays the list of routes defined in the gateway.

    routes/{id}

    GET

    Displays information about a particular route.

    routes/{id}

    POST

    Add a new route to the gateway.

    routes/{id}

    DELETE

    Remove an existing route from the gateway.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__additional_resources.html b/Greenwich.SR5/multi/multi__additional_resources.html new file mode 100644 index 00000000..f859012f --- /dev/null +++ b/Greenwich.SR5/multi/multi__additional_resources.html @@ -0,0 +1,4 @@ + + + 51. Additional Resources

    51. Additional Resources

    You can watch a video of Reshmi Krishna and Marcin Grzejszczak talking about Spring Cloud +Sleuth and Zipkin by clicking here.

    You can check different setups of Sleuth and Brave in the openzipkin/sleuth-webmvc-example repository.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__addressing_all_instances_of_a_service.html b/Greenwich.SR5/multi/multi__addressing_all_instances_of_a_service.html new file mode 100644 index 00000000..e0dcfed1 --- /dev/null +++ b/Greenwich.SR5/multi/multi__addressing_all_instances_of_a_service.html @@ -0,0 +1,6 @@ + + + 45. Addressing All Instances of a Service

    45. Addressing All Instances of a Service

    The destination parameter is used in a Spring PathMatcher (with the path separator +as a colon — :) to determine if an instance processes the message. Using the example +from earlier, /bus-env/customers:** targets all instances of the +customers service regardless of the rest of the service ID.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__addressing_an_instance.html b/Greenwich.SR5/multi/multi__addressing_an_instance.html new file mode 100644 index 00000000..f9bce351 --- /dev/null +++ b/Greenwich.SR5/multi/multi__addressing_an_instance.html @@ -0,0 +1,12 @@ + + + 44. Addressing an Instance

    44. Addressing an Instance

    Each instance of the application has a service ID, whose value can be set with +spring.cloud.bus.id and whose value is expected to be a colon-separated list of +identifiers, in order from least specific to most specific. The default value is +constructed from the environment as a combination of the spring.application.name and +server.port (or spring.application.index, if set). The default value of the ID is +constructed in the form of app:index:id, where:

    • app is the vcap.application.name, if it exists, or spring.application.name
    • index is the vcap.application.instance_index, if it exists, +spring.application.index, local.server.port, server.port, or 0 (in that order).
    • id is the vcap.application.instance_id, if it exists, or a random value.

    The HTTP endpoints accept a destination path parameter, such as +/bus-refresh/customers:9000, where destination is a service ID. If the ID +is owned by an instance on the bus, it processes the message, and all other instances +ignore it.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__apache_kafka_binder.html b/Greenwich.SR5/multi/multi__apache_kafka_binder.html new file mode 100644 index 00000000..86a73dd9 --- /dev/null +++ b/Greenwich.SR5/multi/multi__apache_kafka_binder.html @@ -0,0 +1,311 @@ + + + 39. Apache Kafka Binder

    39. Apache Kafka Binder

    39.1 Usage

    To use Apache Kafka binder, you need to add spring-cloud-stream-binder-kafka as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-kafka</artifactId>
    +</dependency>

    Alternatively, you can also use the Spring Cloud Stream Kafka Starter, as shown inn the following example for Maven:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-starter-stream-kafka</artifactId>
    +</dependency>

    39.2 Apache Kafka Binder Overview

    The following image shows a simplified diagram of how the Apache Kafka binder operates:

    Figure 39.1. Kafka Binder

    kafka binder

    The Apache Kafka Binder implementation maps each destination to an Apache Kafka topic. +The consumer group maps directly to the same Apache Kafka concept. +Partitioning also maps directly to Apache Kafka partitions as well.

    The binder currently uses the Apache Kafka kafka-clients 1.0.0 jar and is designed to be used with a broker of at least that version. +This client can communicate with older brokers (see the Kafka documentation), but certain features may not be available. +For example, with versions earlier than 0.11.x.x, native headers are not supported. +Also, 0.11.x.x does not support the autoAddPartitions property.

    39.3 Configuration Options

    This section contains the configuration options used by the Apache Kafka binder.

    For common configuration options and properties pertaining to binder, see the core documentation.

    39.3.1 Kafka Binder Properties

    spring.cloud.stream.kafka.binder.brokers

    A list of brokers to which the Kafka binder connects.

    Default: localhost.

    spring.cloud.stream.kafka.binder.defaultBrokerPort

    brokers allows hosts specified with or without port information (for example, host1,host2:port2). +This sets the default port when no port is configured in the broker list.

    Default: 9092.

    spring.cloud.stream.kafka.binder.configuration

    Key/Value map of client properties (both producers and consumer) passed to all clients created by the binder. +Due to the fact that these properties are used by both producers and consumers, usage should be restricted to common properties — for example, security settings. +Properties here supersede any properties set in boot.

    Default: Empty map.

    spring.cloud.stream.kafka.binder.consumerProperties

    Key/Value map of arbitrary Kafka client consumer properties. +Properties here supersede any properties set in boot and in the configuration property above.

    Default: Empty map.

    spring.cloud.stream.kafka.binder.headers

    The list of custom headers that are transported by the binder. +Only required when communicating with older applications (⇐ 1.3.x) with a kafka-clients version < 0.11.0.0. Newer versions support headers natively.

    Default: empty.

    spring.cloud.stream.kafka.binder.healthTimeout

    The time to wait to get partition information, in seconds. +Health reports as down if this timer expires.

    Default: 10.

    spring.cloud.stream.kafka.binder.requiredAcks

    The number of required acks on the broker. +See the Kafka documentation for the producer acks property.

    Default: 1.

    spring.cloud.stream.kafka.binder.minPartitionCount

    Effective only if autoCreateTopics or autoAddPartitions is set. +The global minimum number of partitions that the binder configures on topics on which it produces or consumes data. +It can be superseded by the partitionCount setting of the producer or by the value of instanceCount * concurrency settings of the producer (if either is larger).

    Default: 1.

    spring.cloud.stream.kafka.binder.producerProperties

    Key/Value map of arbitrary Kafka client producer properties. +Properties here supersede any properties set in boot and in the configuration property above.

    Default: Empty map.

    spring.cloud.stream.kafka.binder.replicationFactor

    The replication factor of auto-created topics if autoCreateTopics is active. +Can be overridden on each binding.

    Default: 1.

    spring.cloud.stream.kafka.binder.autoCreateTopics

    If set to true, the binder creates new topics automatically. +If set to false, the binder relies on the topics being already configured. +In the latter case, if the topics do not exist, the binder fails to start.

    [Note]Note

    This setting is independent of the auto.topic.create.enable setting of the broker and does not influence it. +If the server is set to auto-create topics, they may be created as part of the metadata retrieval request, with default broker settings.

    Default: true.

    spring.cloud.stream.kafka.binder.autoAddPartitions

    If set to true, the binder creates new partitions if required. +If set to false, the binder relies on the partition size of the topic being already configured. +If the partition count of the target topic is smaller than the expected value, the binder fails to start.

    Default: false.

    spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix

    Enables transactions in the binder. See transaction.id in the Kafka documentation and Transactions in the spring-kafka documentation. +When transactions are enabled, individual producer properties are ignored and all producers use the spring.cloud.stream.kafka.binder.transaction.producer.* properties.

    Default null (no transactions)

    spring.cloud.stream.kafka.binder.transaction.producer.*

    Global producer properties for producers in a transactional binder. +See spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix and Section 39.3.3, “Kafka Producer Properties” and the general producer properties supported by all binders.

    Default: See individual producer properties.

    spring.cloud.stream.kafka.binder.headerMapperBeanName

    The bean name of a KafkaHeaderMapper used for mapping spring-messaging headers to and from Kafka headers. +Use this, for example, if you wish to customize the trusted packages in a DefaultKafkaHeaderMapper that uses JSON deserialization for the headers.

    Default: none.

    39.3.2 Kafka Consumer Properties

    The following properties are available for Kafka consumers only and +must be prefixed with spring.cloud.stream.kafka.bindings.<channelName>.consumer..

    admin.configuration

    A Map of Kafka topic properties used when provisioning topics — for example, spring.cloud.stream.kafka.bindings.input.consumer.admin.configuration.message.format.version=0.9.0.0

    Default: none.

    admin.replicas-assignment

    A Map<Integer, List<Integer>> of replica assignments, with the key being the partition and the value being the assignments. +Used when provisioning new topics. +See the NewTopic Javadocs in the kafka-clients jar.

    Default: none.

    admin.replication-factor

    The replication factor to use when provisioning topics. Overrides the binder-wide setting. +Ignored if replicas-assignments is present.

    Default: none (the binder-wide default of 1 is used).

    autoRebalanceEnabled

    When true, topic partitions is automatically rebalanced between the members of a consumer group. +When false, each consumer is assigned a fixed set of partitions based on spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex. +This requires both the spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex properties to be set appropriately on each launched instance. +The value of the spring.cloud.stream.instanceCount property must typically be greater than 1 in this case.

    Default: true.

    ackEachRecord

    When autoCommitOffset is true, this setting dictates whether to commit the offset after each record is processed. +By default, offsets are committed after all records in the batch of records returned by consumer.poll() have been processed. +The number of records returned by a poll can be controlled with the max.poll.records Kafka property, which is set through the consumer configuration property. +Setting this to true may cause a degradation in performance, but doing so reduces the likelihood of redelivered records when a failure occurs. +Also, see the binder requiredAcks property, which also affects the performance of committing offsets.

    Default: false.

    autoCommitOffset

    Whether to autocommit offsets when a message has been processed. +If set to false, a header with the key kafka_acknowledgment of the type org.springframework.kafka.support.Acknowledgment header is present in the inbound message. +Applications may use this header for acknowledging messages. +See the examples section for details. +When this property is set to false, Kafka binder sets the ack mode to org.springframework.kafka.listener.AbstractMessageListenerContainer.AckMode.MANUAL and the application is responsible for acknowledging records. +Also see ackEachRecord.

    Default: true.

    autoCommitOnError

    Effective only if autoCommitOffset is set to true. +If set to false, it suppresses auto-commits for messages that result in errors and commits only for successful messages. It allows a stream to automatically replay from the last successfully processed message, in case of persistent failures. +If set to true, it always auto-commits (if auto-commit is enabled). +If not set (the default), it effectively has the same value as enableDlq, auto-committing erroneous messages if they are sent to a DLQ and not committing them otherwise.

    Default: not set.

    resetOffsets

    Whether to reset offsets on the consumer to the value provided by startOffset.

    Default: false.

    startOffset

    The starting offset for new groups. +Allowed values: earliest and latest. +If the consumer group is set explicitly for the consumer 'binding' (through spring.cloud.stream.bindings.<channelName>.group), 'startOffset' is set to earliest. Otherwise, it is set to latest for the anonymous consumer group. +Also see resetOffsets (earlier in this list).

    Default: null (equivalent to earliest).

    enableDlq

    When set to true, it enables DLQ behavior for the consumer. +By default, messages that result in errors are forwarded to a topic named error.<destination>.<group>. +The DLQ topic name can be configurable by setting the dlqName property. +This provides an alternative option to the more common Kafka replay scenario for the case when the number of errors is relatively small and replaying the entire original topic may be too cumbersome. +See Section 39.6, “Dead-Letter Topic Processing” processing for more information. +Starting with version 2.0, messages sent to the DLQ topic are enhanced with the following headers: x-original-topic, x-exception-message, and x-exception-stacktrace as byte[]. +Not allowed when destinationIsPattern is true.

    Default: false.

    configuration

    Map with a key/value pair containing generic Kafka consumer properties.

    Default: Empty map.

    dlqName

    The name of the DLQ topic to receive the error messages.

    Default: null (If not specified, messages that result in errors are forwarded to a topic named error.<destination>.<group>).

    dlqProducerProperties

    Using this, DLQ-specific producer properties can be set. +All the properties available through kafka producer properties can be set through this property.

    Default: Default Kafka producer properties.

    standardHeaders

    Indicates which standard headers are populated by the inbound channel adapter. +Allowed values: none, id, timestamp, or both. +Useful if using native deserialization and the first component to receive a message needs an id (such as an aggregator that is configured to use a JDBC message store).

    Default: none

    converterBeanName

    The name of a bean that implements RecordMessageConverter. Used in the inbound channel adapter to replace the default MessagingMessageConverter.

    Default: null

    idleEventInterval

    The interval, in milliseconds, between events indicating that no messages have recently been received. +Use an ApplicationListener<ListenerContainerIdleEvent> to receive these events. +See the section called “Example: Pausing and Resuming the Consumer” for a usage example.

    Default: 30000

    destinationIsPattern

    When true, the destination is treated as a regular expression Pattern used to match topic names by the broker. +When true, topics are not provisioned, and enableDlq is not allowed, because the binder does not know the topic names during the provisioning phase. +Note, the time taken to detect new topics that match the pattern is controlled by the consumer property metadata.max.age.ms, which (at the time of writing) defaults to 300,000ms (5 minutes). +This can be configured using the configuration property above.

    Default: false

    39.3.3 Kafka Producer Properties

    The following properties are available for Kafka producers only and +must be prefixed with spring.cloud.stream.kafka.bindings.<channelName>.producer..

    admin.configuration

    A Map of Kafka topic properties used when provisioning new topics — for example, spring.cloud.stream.kafka.bindings.input.consumer.admin.configuration.message.format.version=0.9.0.0

    Default: none.

    admin.replicas-assignment

    A Map<Integer, List<Integer>> of replica assignments, with the key being the partition and the value being the assignments. +Used when provisioning new topics. +See NewTopic javadocs in the kafka-clients jar.

    Default: none.

    admin.replication-factor

    The replication factor to use when provisioning new topics. Overrides the binder-wide setting. +Ignored if replicas-assignments is present.

    Default: none (the binder-wide default of 1 is used).

    bufferSize

    Upper limit, in bytes, of how much data the Kafka producer attempts to batch before sending.

    Default: 16384.

    sync

    Whether the producer is synchronous.

    Default: false.

    batchTimeout

    How long the producer waits to allow more messages to accumulate in the same batch before sending the messages. +(Normally, the producer does not wait at all and simply sends all the messages that accumulated while the previous send was in progress.) A non-zero value may increase throughput at the expense of latency.

    Default: 0.

    messageKeyExpression

    A SpEL expression evaluated against the outgoing message used to populate the key of the produced Kafka message — for example, headers['myKey']. +The payload cannot be used because, by the time this expression is evaluated, the payload is already in the form of a byte[].

    Default: none.

    headerPatterns

    A comma-delimited list of simple patterns to match Spring messaging headers to be mapped to the Kafka Headers in the ProducerRecord. +Patterns can begin or end with the wildcard character (asterisk). +Patterns can be negated by prefixing with !. +Matching stops after the first match (positive or negative). +For example !ask,as* will pass ash but not ask. +id and timestamp are never mapped.

    Default: * (all headers - except the id and timestamp)

    configuration

    Map with a key/value pair containing generic Kafka producer properties.

    Default: Empty map.

    [Note]Note

    The Kafka binder uses the partitionCount setting of the producer as a hint to create a topic with the given partition count (in conjunction with the minPartitionCount, the maximum of the two being the value being used). +Exercise caution when configuring both minPartitionCount for a binder and partitionCount for an application, as the larger value is used. +If a topic already exists with a smaller partition count and autoAddPartitions is disabled (the default), the binder fails to start. +If a topic already exists with a smaller partition count and autoAddPartitions is enabled, new partitions are added. +If a topic already exists with a larger number of partitions than the maximum of (minPartitionCount or partitionCount), the existing partition count is used.

    39.3.4 Usage examples

    In this section, we show the use of the preceding properties for specific scenarios.

    Example: Setting autoCommitOffset to false and Relying on Manual Acking

    This example illustrates how one may manually acknowledge offsets in a consumer application.

    This example requires that spring.cloud.stream.kafka.bindings.input.consumer.autoCommitOffset be set to false. +Use the corresponding input channel name for your example.

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class ManuallyAcknowdledgingConsumer {
    +
    + public static void main(String[] args) {
    +     SpringApplication.run(ManuallyAcknowdledgingConsumer.class, args);
    + }
    +
    + @StreamListener(Sink.INPUT)
    + public void process(Message<?> message) {
    +     Acknowledgment acknowledgment = message.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class);
    +     if (acknowledgment != null) {
    +         System.out.println("Acknowledgment provided");
    +         acknowledgment.acknowledge();
    +     }
    + }
    +}

    Example: Security Configuration

    Apache Kafka 0.9 supports secure connections between client and brokers. +To take advantage of this feature, follow the guidelines in the Apache Kafka Documentation as well as the Kafka 0.9 security guidelines from the Confluent documentation. +Use the spring.cloud.stream.kafka.binder.configuration option to set security properties for all clients created by the binder.

    For example, to set security.protocol to SASL_SSL, set the following property:

    spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_SSL

    All the other security properties can be set in a similar manner.

    When using Kerberos, follow the instructions in the reference documentation for creating and referencing the JAAS configuration.

    Spring Cloud Stream supports passing JAAS configuration information to the application by using a JAAS configuration file and using Spring Boot properties.

    Using JAAS Configuration Files

    The JAAS and (optionally) krb5 file locations can be set for Spring Cloud Stream applications by using system properties. +The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using a JAAS configuration file:

     java -Djava.security.auth.login.config=/path.to/kafka_client_jaas.conf -jar log.jar \
    +   --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \
    +   --spring.cloud.stream.bindings.input.destination=stream.ticktock \
    +   --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT
    Using Spring Boot Properties

    As an alternative to having a JAAS configuration file, Spring Cloud Stream provides a mechanism for setting up the JAAS configuration for Spring Cloud Stream applications by using Spring Boot properties.

    The following properties can be used to configure the login context of the Kafka client:

    spring.cloud.stream.kafka.binder.jaas.loginModule

    The login module name. Not necessary to be set in normal cases.

    Default: com.sun.security.auth.module.Krb5LoginModule.

    spring.cloud.stream.kafka.binder.jaas.controlFlag

    The control flag of the login module.

    Default: required.

    spring.cloud.stream.kafka.binder.jaas.options

    Map with a key/value pair containing the login module options.

    Default: Empty map.

    The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using Spring Boot configuration properties:

     java --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \
    +   --spring.cloud.stream.bindings.input.destination=stream.ticktock \
    +   --spring.cloud.stream.kafka.binder.autoCreateTopics=false \
    +   --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT \
    +   --spring.cloud.stream.kafka.binder.jaas.options.useKeyTab=true \
    +   --spring.cloud.stream.kafka.binder.jaas.options.storeKey=true \
    +   --spring.cloud.stream.kafka.binder.jaas.options.keyTab=/etc/security/keytabs/kafka_client.keytab \
    +   --spring.cloud.stream.kafka.binder.jaas.options.principal=kafka-client-1@EXAMPLE.COM

    The preceding example represents the equivalent of the following JAAS file:

    KafkaClient {
    +    com.sun.security.auth.module.Krb5LoginModule required
    +    useKeyTab=true
    +    storeKey=true
    +    keyTab="/etc/security/keytabs/kafka_client.keytab"
    +    principal="kafka-client-1@EXAMPLE.COM";
    +};

    If the topics required already exist on the broker or will be created by an administrator, autocreation can be turned off and only client JAAS properties need to be sent.

    [Note]Note

    Do not mix JAAS configuration files and Spring Boot properties in the same application. +If the -Djava.security.auth.login.config system property is already present, Spring Cloud Stream ignores the Spring Boot properties.

    [Note]Note

    Be careful when using the autoCreateTopics and autoAddPartitions with Kerberos. +Usually, applications may use principals that do not have administrative rights in Kafka and Zookeeper. +Consequently, relying on Spring Cloud Stream to create/modify topics may fail. +In secure environments, we strongly recommend creating topics and managing ACLs administratively by using Kafka tooling.

    Example: Pausing and Resuming the Consumer

    If you wish to suspend consumption but not cause a partition rebalance, you can pause and resume the consumer. +This is facilitated by adding the Consumer as a parameter to your @StreamListener. +To resume, you need an ApplicationListener for ListenerContainerIdleEvent instances. +The frequency at which events are published is controlled by the idleEventInterval property. +Since the consumer is not thread-safe, you must call these methods on the calling thread.

    The following simple application shows how to pause and resume:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class Application {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(Application.class, args);
    +	}
    +
    +	@StreamListener(Sink.INPUT)
    +	public void in(String in, @Header(KafkaHeaders.CONSUMER) Consumer<?, ?> consumer) {
    +		System.out.println(in);
    +		consumer.pause(Collections.singleton(new TopicPartition("myTopic", 0)));
    +	}
    +
    +	@Bean
    +	public ApplicationListener<ListenerContainerIdleEvent> idleListener() {
    +		return event -> {
    +			System.out.println(event);
    +			if (event.getConsumer().paused().size() > 0) {
    +				event.getConsumer().resume(event.getConsumer().paused());
    +			}
    +		};
    +	}
    +
    +}

    39.4 Error Channels

    Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See Section 29.4, “Error Handling” for more information.

    The payload of the ErrorMessage for a send failure is a KafkaSendFailureException with properties:

    • failedMessage: The Spring Messaging Message<?> that failed to be sent.
    • record: The raw ProducerRecord that was created from the failedMessage

    There is no automatic handling of producer exceptions (such as sending to a Dead-Letter queue). +You can consume these exceptions with your own Spring Integration flow.

    39.5 Kafka Metrics

    Kafka binder module exposes the following metrics:

    spring.cloud.stream.binder.kafka.offset: This metric indicates how many messages have not been yet consumed from a given binder’s topic by a given consumer group. +The metrics provided are based on the Mircometer metrics library. The metric contains the consumer group information, topic and the actual lag in committed offset from the latest offset on the topic. +This metric is particularly useful for providing auto-scaling feedback to a PaaS platform.

    39.6 Dead-Letter Topic Processing

    Because you cannot anticipate how users would want to dispose of dead-lettered messages, the framework does not provide any standard mechanism to handle them. +If the reason for the dead-lettering is transient, you may wish to route the messages back to the original topic. +However, if the problem is a permanent issue, that could cause an infinite loop. +The sample Spring Boot application within this topic is an example of how to route those messages back to the original topic, but it moves them to a parking lot topic after three attempts. +The application is another spring-cloud-stream application that reads from the dead-letter topic. +It terminates when no messages are received for 5 seconds.

    The examples assume the original destination is so8400out and the consumer group is so8400.

    There are a couple of strategies to consider:

    • Consider running the rerouting only when the main application is not running. +Otherwise, the retries for transient errors are used up very quickly.
    • Alternatively, use a two-stage approach: Use this application to route to a third topic and another to route from there back to the main topic.

    The following code listings show the sample application:

    application.properties.  +

    spring.cloud.stream.bindings.input.group=so8400replay
    +spring.cloud.stream.bindings.input.destination=error.so8400out.so8400
    +
    +spring.cloud.stream.bindings.output.destination=so8400out
    +spring.cloud.stream.bindings.output.producer.partitioned=true
    +
    +spring.cloud.stream.bindings.parkingLot.destination=so8400in.parkingLot
    +spring.cloud.stream.bindings.parkingLot.producer.partitioned=true
    +
    +spring.cloud.stream.kafka.binder.configuration.auto.offset.reset=earliest
    +
    +spring.cloud.stream.kafka.binder.headers=x-retries

    +

    Application.  +

    @SpringBootApplication
    +@EnableBinding(TwoOutputProcessor.class)
    +public class ReRouteDlqKApplication implements CommandLineRunner {
    +
    +    private static final String X_RETRIES_HEADER = "x-retries";
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(ReRouteDlqKApplication.class, args).close();
    +    }
    +
    +    private final AtomicInteger processed = new AtomicInteger();
    +
    +    @Autowired
    +    private MessageChannel parkingLot;
    +
    +    @StreamListener(Processor.INPUT)
    +    @SendTo(Processor.OUTPUT)
    +    public Message<?> reRoute(Message<?> failed) {
    +        processed.incrementAndGet();
    +        Integer retries = failed.getHeaders().get(X_RETRIES_HEADER, Integer.class);
    +        if (retries == null) {
    +            System.out.println("First retry for " + failed);
    +            return MessageBuilder.fromMessage(failed)
    +                    .setHeader(X_RETRIES_HEADER, new Integer(1))
    +                    .setHeader(BinderHeaders.PARTITION_OVERRIDE,
    +                            failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
    +                    .build();
    +        }
    +        else if (retries.intValue() < 3) {
    +            System.out.println("Another retry for " + failed);
    +            return MessageBuilder.fromMessage(failed)
    +                    .setHeader(X_RETRIES_HEADER, new Integer(retries.intValue() + 1))
    +                    .setHeader(BinderHeaders.PARTITION_OVERRIDE,
    +                            failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
    +                    .build();
    +        }
    +        else {
    +            System.out.println("Retries exhausted for " + failed);
    +            parkingLot.send(MessageBuilder.fromMessage(failed)
    +                    .setHeader(BinderHeaders.PARTITION_OVERRIDE,
    +                            failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
    +                    .build());
    +        }
    +        return null;
    +    }
    +
    +    @Override
    +    public void run(String... args) throws Exception {
    +        while (true) {
    +            int count = this.processed.get();
    +            Thread.sleep(5000);
    +            if (count == this.processed.get()) {
    +                System.out.println("Idle, terminating");
    +                return;
    +            }
    +        }
    +    }
    +
    +    public interface TwoOutputProcessor extends Processor {
    +
    +        @Output("parkingLot")
    +        MessageChannel parkingLot();
    +
    +    }
    +
    +}

    +

    39.7 Partitioning with the Kafka Binder

    Apache Kafka supports topic partitioning natively.

    Sometimes it is advantageous to send data to specific partitions — for example, when you want to strictly order message processing (all messages for a particular customer should go to the same partition).

    The following example shows how to configure the producer and consumer side:

    @SpringBootApplication
    +@EnableBinding(Source.class)
    +public class KafkaPartitionProducerApplication {
    +
    +    private static final Random RANDOM = new Random(System.currentTimeMillis());
    +
    +    private static final String[] data = new String[] {
    +            "foo1", "bar1", "qux1",
    +            "foo2", "bar2", "qux2",
    +            "foo3", "bar3", "qux3",
    +            "foo4", "bar4", "qux4",
    +            };
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(KafkaPartitionProducerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedRate = "5000"))
    +    public Message<?> generate() {
    +        String value = data[RANDOM.nextInt(data.length)];
    +        System.out.println("Sending: " + value);
    +        return MessageBuilder.withPayload(value)
    +                .setHeader("partitionKey", value)
    +                .build();
    +    }
    +
    +}

    application.yml.  +

    spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        output:
    +          destination: partitioned.topic
    +          producer:
    +            partitioned: true
    +            partition-key-expression: headers['partitionKey']
    +            partition-count: 12

    +

    [Important]Important

    The topic must be provisioned to have enough partitions to achieve the desired concurrency for all consumer groups. +The above configuration supports up to 12 consumer instances (6 if their concurrency is 2, 4 if their concurrency is 3, and so on). +It is generally best to over-provision the partitions to allow for future increases in consumers or concurrency.

    [Note]Note

    The preceding configuration uses the default partitioning (key.hashCode() % partitionCount). +This may or may not provide a suitably balanced algorithm, depending on the key values. +You can override this default by using the partitionSelectorExpression or partitionSelectorClass properties.

    Since partitions are natively handled by Kafka, no special configuration is needed on the consumer side. +Kafka allocates partitions across the instances.

    The following Spring Boot application listens to a Kafka stream and prints (to the console) the partition ID to which each message goes:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class KafkaPartitionConsumerApplication {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(KafkaPartitionConsumerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @StreamListener(Sink.INPUT)
    +    public void listen(@Payload String in, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
    +        System.out.println(in + " received from partition " + partition);
    +    }
    +
    +}

    application.yml.  +

    spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        input:
    +          destination: partitioned.topic
    +          group: myGroup

    +

    You can add instances as needed. +Kafka rebalances the partition allocations. +If the instance count (or instance count * concurrency) exceeds the number of partitions, some consumers are idle.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__apache_kafka_streams_binder.html b/Greenwich.SR5/multi/multi__apache_kafka_streams_binder.html new file mode 100644 index 00000000..910b3e51 --- /dev/null +++ b/Greenwich.SR5/multi/multi__apache_kafka_streams_binder.html @@ -0,0 +1,276 @@ + + + 40. Apache Kafka Streams Binder

    40. Apache Kafka Streams Binder

    40.1 Usage

    For using the Kafka Streams binder, you just need to add it to your Spring Cloud Stream application, using the following +Maven coordinates:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
    +</dependency>

    40.2 Kafka Streams Binder Overview

    Spring Cloud Stream’s Apache Kafka support also includes a binder implementation designed explicitly for Apache Kafka +Streams binding. With this native integration, a Spring Cloud Stream "processor" application can directly use the +Apache Kafka Streams APIs in the core business logic.

    Kafka Streams binder implementation builds on the foundation provided by the Kafka Streams in Spring Kafka +project.

    Kafka Streams binder provides binding capabilities for the three major types in Kafka Streams - KStream, KTable and GlobalKTable.

    As part of this native integration, the high-level Streams DSL +provided by the Kafka Streams API is available for use in the business logic.

    An early version of the Processor API +support is available as well.

    As noted early-on, Kafka Streams support in Spring Cloud Stream is strictly only available for use in the Processor model. +A model in which the messages read from an inbound topic, business processing can be applied, and the transformed messages +can be written to an outbound topic. It can also be used in Processor applications with a no-outbound destination.

    40.2.1 Streams DSL

    This application consumes data from a Kafka topic (e.g., words), computes word count for each unique word in a 5 seconds +time window, and the computed results are sent to a downstream topic (e.g., counts) for further processing.

    @SpringBootApplication
    +@EnableBinding(KStreamProcessor.class)
    +public class WordCountProcessorApplication {
    +
    +	@StreamListener("input")
    +	@SendTo("output")
    +	public KStream<?, WordCount> process(KStream<?, String> input) {
    +		return input
    +                .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
    +                .groupBy((key, value) -> value)
    +                .windowedBy(TimeWindows.of(5000))
    +                .count(Materialized.as("WordCounts-multi"))
    +                .toStream()
    +                .map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, new Date(key.window().start()), new Date(key.window().end()))));
    +    }
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(WordCountProcessorApplication.class, args);
    +	}

    Once built as a uber-jar (e.g., wordcount-processor.jar), you can run the above example like the following.

    java -jar wordcount-processor.jar  --spring.cloud.stream.bindings.input.destination=words --spring.cloud.stream.bindings.output.destination=counts

    This application will consume messages from the Kafka topic words and the computed results are published to an output +topic counts.

    Spring Cloud Stream will ensure that the messages from both the incoming and outgoing topics are automatically bound as +KStream objects. As a developer, you can exclusively focus on the business aspects of the code, i.e. writing the logic +required in the processor. Setting up the Streams DSL specific configuration required by the Kafka Streams infrastructure +is automatically handled by the framework.

    40.3 Configuration Options

    This section contains the configuration options used by the Kafka Streams binder.

    For common configuration options and properties pertaining to binder, refer to the core documentation.

    40.3.1 Kafka Streams Properties

    The following properties are available at the binder level and must be prefixed with spring.cloud.stream.kafka.streams.binder. +literal.

    configuration
    Map with a key/value pair containing properties pertaining to Apache Kafka Streams API. + This property must be prefixed with spring.cloud.stream.kafka.streams.binder.. +Following are some examples of using this property.
    spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
    +spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
    +spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000

    For more information about all the properties that may go into streams configuration, see StreamsConfig JavaDocs in +Apache Kafka Streams docs.

    brokers

    Broker URL

    Default: localhost

    zkNodes

    Zookeeper URL

    Default: localhost

    serdeError

    Deserialization error handler type. +Possible values are - logAndContinue, logAndFail or sendToDlq

    Default: logAndFail

    applicationId

    Convenient way to set the application.id for the Kafka Streams application globally at the binder level. +If the application contains multiple StreamListener methods, then application.id should be set at the binding level per input binding.

    Default: none

    The following properties are only available for Kafka Streams producers and must be prefixed with spring.cloud.stream.kafka.streams.bindings.<binding name>.producer. literal. +For convenience, if there multiple output bindings and they all require a common value, that can be configured by using the prefix spring.cloud.stream.kafka.streams.default.producer..

    keySerde

    key serde to use

    Default: none.

    valueSerde

    value serde to use

    Default: none.

    useNativeEncoding

    flag to enable native encoding

    Default: false.

    The following properties are only available for Kafka Streams consumers and must be prefixed with spring.cloud.stream.kafka.streams.bindings.<binding name>.consumer.`literal. +For convenience, if there multiple input bindings and they all require a common value, that can be configured by using the prefix `spring.cloud.stream.kafka.streams.default.consumer..

    applicationId

    Setting application.id per input binding.

    Default: none

    keySerde

    key serde to use

    Default: none.

    valueSerde

    value serde to use

    Default: none.

    materializedAs

    state store to materialize when using incoming KTable types

    Default: none.

    useNativeDecoding

    flag to enable native decoding

    Default: false.

    dlqName

    DLQ topic name.

    Default: none.

    40.3.2 TimeWindow properties:

    Windowing is an important concept in stream processing applications. Following properties are available to configure +time-window computations.

    spring.cloud.stream.kafka.streams.timeWindow.length

    When this property is given, you can autowire a TimeWindows bean into the application. +The value is expressed in milliseconds.

    Default: none.

    spring.cloud.stream.kafka.streams.timeWindow.advanceBy

    Value is given in milliseconds.

    Default: none.

    40.4 Multiple Input Bindings

    For use cases that requires multiple incoming KStream objects or a combination of KStream and KTable objects, the Kafka +Streams binder provides multiple bindings support.

    Let’s see it in action.

    40.4.1 Multiple Input Bindings as a Sink

    @EnableBinding(KStreamKTableBinding.class)
    +.....
    +.....
    +@StreamListener
    +public void process(@Input("inputStream") KStream<String, PlayEvent> playEvents,
    +                    @Input("inputTable") KTable<Long, Song> songTable) {
    +                    ....
    +                    ....
    +}
    +
    +interface KStreamKTableBinding {
    +
    +    @Input("inputStream")
    +    KStream<?, ?> inputStream();
    +
    +    @Input("inputTable")
    +    KTable<?, ?> inputTable();
    +}

    In the above example, the application is written as a sink, i.e. there are no output bindings and the application has to +decide concerning downstream processing. When you write applications in this style, you might want to send the information +downstream or store them in a state store (See below for Queryable State Stores).

    In the case of incoming KTable, if you want to materialize the computations to a state store, you have to express it +through the following property.

    spring.cloud.stream.kafka.streams.bindings.inputTable.consumer.materializedAs: all-songs

    The above example shows the use of KTable as an input binding. +The binder also supports input bindings for GlobalKTable. +GlobalKTable binding is useful when you have to ensure that all instances of your application has access to the data updates from the topic. +KTable and GlobalKTable bindings are only available on the input. +Binder supports both input and output bindings for KStream.

    40.4.2 Multiple Input Bindings as a Processor

    @EnableBinding(KStreamKTableBinding.class)
    +....
    +....
    +
    +@StreamListener
    +@SendTo("output")
    +public KStream<String, Long> process(@Input("input") KStream<String, Long> userClicksStream,
    +                                     @Input("inputTable") KTable<String, String> userRegionsTable) {
    +....
    +....
    +}
    +
    +interface KStreamKTableBinding extends KafkaStreamsProcessor {
    +
    +    @Input("inputX")
    +    KTable<?, ?> inputTable();
    +}

    40.5 Multiple Output Bindings (aka Branching)

    Kafka Streams allow outbound data to be split into multiple topics based on some predicates. The Kafka Streams binder provides +support for this feature without compromising the programming model exposed through StreamListener in the end user application.

    You can write the application in the usual way as demonstrated above in the word count example. However, when using the +branching feature, you are required to do a few things. First, you need to make sure that your return type is KStream[] +instead of a regular KStream. Second, you need to use the SendTo annotation containing the output bindings in the order +(see example below). For each of these output bindings, you need to configure destination, content-type etc., complying with +the standard Spring Cloud Stream expectations.

    Here is an example:

    @EnableBinding(KStreamProcessorWithBranches.class)
    +@EnableAutoConfiguration
    +public static class WordCountProcessorApplication {
    +
    +    @Autowired
    +    private TimeWindows timeWindows;
    +
    +    @StreamListener("input")
    +    @SendTo({"output1","output2","output3})
    +    public KStream<?, WordCount>[] process(KStream<Object, String> input) {
    +
    +			Predicate<Object, WordCount> isEnglish = (k, v) -> v.word.equals("english");
    +			Predicate<Object, WordCount> isFrench =  (k, v) -> v.word.equals("french");
    +			Predicate<Object, WordCount> isSpanish = (k, v) -> v.word.equals("spanish");
    +
    +			return input
    +					.flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
    +					.groupBy((key, value) -> value)
    +					.windowedBy(timeWindows)
    +					.count(Materialized.as("WordCounts-1"))
    +					.toStream()
    +					.map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, new Date(key.window().start()), new Date(key.window().end()))))
    +					.branch(isEnglish, isFrench, isSpanish);
    +    }
    +
    +    interface KStreamProcessorWithBranches {
    +
    +    		@Input("input")
    +    		KStream<?, ?> input();
    +
    +    		@Output("output1")
    +    		KStream<?, ?> output1();
    +
    +    		@Output("output2")
    +    		KStream<?, ?> output2();
    +
    +    		@Output("output3")
    +    		KStream<?, ?> output3();
    +    	}
    +}

    Properties:

    spring.cloud.stream.bindings.output1.contentType: application/json
    +spring.cloud.stream.bindings.output2.contentType: application/json
    +spring.cloud.stream.bindings.output3.contentType: application/json
    +spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms: 1000
    +spring.cloud.stream.kafka.streams.binder.configuration:
    +  default.key.serde: org.apache.kafka.common.serialization.Serdes$StringSerde
    +  default.value.serde: org.apache.kafka.common.serialization.Serdes$StringSerde
    +spring.cloud.stream.bindings.output1:
    +  destination: foo
    +  producer:
    +    headerMode: raw
    +spring.cloud.stream.bindings.output2:
    +  destination: bar
    +  producer:
    +    headerMode: raw
    +spring.cloud.stream.bindings.output3:
    +  destination: fox
    +  producer:
    +    headerMode: raw
    +spring.cloud.stream.bindings.input:
    +  destination: words
    +  consumer:
    +    headerMode: raw

    40.6 Message Conversion

    Similar to message-channel based binder applications, the Kafka Streams binder adapts to the out-of-the-box content-type +conversions without any compromise.

    It is typical for Kafka Streams operations to know the type of SerDe’s used to transform the key and value correctly. +Therefore, it may be more natural to rely on the SerDe facilities provided by the Apache Kafka Streams library itself at +the inbound and outbound conversions rather than using the content-type conversions offered by the framework. +On the other hand, you might be already familiar with the content-type conversion patterns provided by the framework, and +that, you’d like to continue using for inbound and outbound conversions.

    Both the options are supported in the Kafka Streams binder implementation.

    40.6.1 Outbound serialization

    If native encoding is disabled (which is the default), then the framework will convert the message using the contentType +set by the user (otherwise, the default application/json will be applied). It will ignore any SerDe set on the outbound +in this case for outbound serialization.

    Here is the property to set the contentType on the outbound.

    spring.cloud.stream.bindings.output.contentType: application/json

    Here is the property to enable native encoding.

    spring.cloud.stream.bindings.output.nativeEncoding: true

    If native encoding is enabled on the output binding (user has to enable it as above explicitly), then the framework will +skip any form of automatic message conversion on the outbound. In that case, it will switch to the Serde set by the user. +The valueSerde property set on the actual output binding will be used. Here is an example.

    spring.cloud.stream.kafka.streams.bindings.output.producer.valueSerde: org.apache.kafka.common.serialization.Serdes$StringSerde

    If this property is not set, then it will use the "default" SerDe: spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde.

    It is worth to mention that Kafka Streams binder does not serialize the keys on outbound - it simply relies on Kafka itself. +Therefore, you either have to specify the keySerde property on the binding or it will default to the application-wide common +keySerde.

    Binding level key serde:

    spring.cloud.stream.kafka.streams.bindings.output.producer.keySerde

    Common Key serde:

    spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde

    If branching is used, then you need to use multiple output bindings. For example,

    interface KStreamProcessorWithBranches {
    +
    +    		@Input("input")
    +    		KStream<?, ?> input();
    +
    +    		@Output("output1")
    +    		KStream<?, ?> output1();
    +
    +    		@Output("output2")
    +    		KStream<?, ?> output2();
    +
    +    		@Output("output3")
    +    		KStream<?, ?> output3();
    +    	}

    If nativeEncoding is set, then you can set different SerDe’s on individual output bindings as below.

    spring.cloud.stream.kafka.streams.bindings.output1.producer.valueSerde=IntegerSerde
    +spring.cloud.stream.kafka.streams.bindings.output2.producer.valueSerde=StringSerde
    +spring.cloud.stream.kafka.streams.bindings.output3.producer.valueSerde=JsonSerde

    Then if you have SendTo like this, @SendTo({"output1", "output2", "output3"}), the KStream[] from the branches are +applied with proper SerDe objects as defined above. If you are not enabling nativeEncoding, you can then set different +contentType values on the output bindings as below. In that case, the framework will use the appropriate message converter +to convert the messages before sending to Kafka.

    spring.cloud.stream.bindings.output1.contentType: application/json
    +spring.cloud.stream.bindings.output2.contentType: application/java-serialzied-object
    +spring.cloud.stream.bindings.output3.contentType: application/octet-stream

    40.6.2 Inbound Deserialization

    Similar rules apply to data deserialization on the inbound.

    If native decoding is disabled (which is the default), then the framework will convert the message using the contentType +set by the user (otherwise, the default application/json will be applied). It will ignore any SerDe set on the inbound +in this case for inbound deserialization.

    Here is the property to set the contentType on the inbound.

    spring.cloud.stream.bindings.input.contentType: application/json

    Here is the property to enable native decoding.

    spring.cloud.stream.bindings.input.nativeDecoding: true

    If native decoding is enabled on the input binding (user has to enable it as above explicitly), then the framework will +skip doing any message conversion on the inbound. In that case, it will switch to the SerDe set by the user. The valueSerde +property set on the actual output binding will be used. Here is an example.

    spring.cloud.stream.kafka.streams.bindings.input.consumer.valueSerde: org.apache.kafka.common.serialization.Serdes$StringSerde

    If this property is not set, it will use the default SerDe: spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde.

    It is worth to mention that Kafka Streams binder does not deserialize the keys on inbound - it simply relies on Kafka itself. +Therefore, you either have to specify the keySerde property on the binding or it will default to the application-wide common +keySerde.

    Binding level key serde:

    spring.cloud.stream.kafka.streams.bindings.input.consumer.keySerde

    Common Key serde:

    spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde

    As in the case of KStream branching on the outbound, the benefit of setting value SerDe per binding is that if you have +multiple input bindings (multiple KStreams object) and they all require separate value SerDe’s, then you can configure +them individually. If you use the common configuration approach, then this feature won’t be applicable.

    40.7 Error Handling

    Apache Kafka Streams provide the capability for natively handling exceptions from deserialization errors. +For details on this support, please see this +Out of the box, Apache Kafka Streams provide two kinds of deserialization exception handlers - logAndContinue and logAndFail. +As the name indicates, the former will log the error and continue processing the next records and the latter will log the +error and fail. LogAndFail is the default deserialization exception handler.

    40.7.1 Handling Deserialization Exceptions

    Kafka Streams binder supports a selection of exception handlers through the following properties.

    spring.cloud.stream.kafka.streams.binder.serdeError: logAndContinue

    In addition to the above two deserialization exception handlers, the binder also provides a third one for sending the erroneous +records (poison pills) to a DLQ topic. Here is how you enable this DLQ exception handler.

    spring.cloud.stream.kafka.streams.binder.serdeError: sendToDlq

    When the above property is set, all the deserialization error records are automatically sent to the DLQ topic.

    spring.cloud.stream.kafka.streams.bindings.input.consumer.dlqName: foo-dlq

    If this is set, then the error records are sent to the topic foo-dlq. If this is not set, then it will create a DLQ +topic with the name error.<input-topic-name>.<group-name>.

    A couple of things to keep in mind when using the exception handling feature in Kafka Streams binder.

    • The property spring.cloud.stream.kafka.streams.binder.serdeError is applicable for the entire application. This implies +that if there are multiple StreamListener methods in the same application, this property is applied to all of them.
    • The exception handling for deserialization works consistently with native deserialization and framework provided message +conversion.

    40.7.2 Handling Non-Deserialization Exceptions

    For general error handling in Kafka Streams binder, it is up to the end user applications to handle application level errors. +As a side effect of providing a DLQ for deserialization exception handlers, Kafka Streams binder provides a way to get +access to the DLQ sending bean directly from your application. +Once you get access to that bean, you can programmatically send any exception records from your application to the DLQ.

    It continues to remain hard to robust error handling using the high-level DSL; Kafka Streams doesn’t natively support error +handling yet.

    However, when you use the low-level Processor API in your application, there are options to control this behavior. See +below.

    @Autowired
    +private SendToDlqAndContinue dlqHandler;
    +
    +@StreamListener("input")
    +@SendTo("output")
    +public KStream<?, WordCount> process(KStream<Object, String> input) {
    +
    +    input.process(() -> new Processor() {
    +    			ProcessorContext context;
    +
    +    			@Override
    +    			public void init(ProcessorContext context) {
    +    				this.context = context;
    +    			}
    +
    +    			@Override
    +    			public void process(Object o, Object o2) {
    +
    +    			    try {
    +    			        .....
    +    			        .....
    +    			    }
    +    			    catch(Exception e) {
    +    			        //explicitly provide the kafka topic corresponding to the input binding as the first argument.
    +                        //DLQ handler will correctly map to the dlq topic from the actual incoming destination.
    +                        dlqHandler.sendToDlq("topic-name", (byte[]) o1, (byte[]) o2, context.partition());
    +    			    }
    +    			}
    +
    +    			.....
    +    			.....
    +    });
    +}

    40.8 State Store

    State store is created automatically by Kafka Streams when the DSL is used. +When processor API is used, you need to register a state store manually. In order to do so, you can use KafkaStreamsStateStore annotation. +You can specify the name and type of the store, flags to control log and disabling cache, etc. +Once the store is created by the binder during the bootstrapping phase, you can access this state store through the processor API. +Below are some primitives for doing this.

    Creating a state store:

    @KafkaStreamsStateStore(name="mystate", type= KafkaStreamsStateStoreProperties.StoreType.WINDOW, lengthMs=300000)
    +public void process(KStream<Object, Product> input) {
    +    ...
    +}

    Accessing the state store:

    Processor<Object, Product>() {
    +
    +    WindowStore<Object, String> state;
    +
    +    @Override
    +    public void init(ProcessorContext processorContext) {
    +        state = (WindowStore)processorContext.getStateStore("mystate");
    +    }
    +    ...
    +}

    40.9 Interactive Queries

    As part of the public Kafka Streams binder API, we expose a class called InteractiveQueryService. +You can access this as a Spring bean in your application. An easy way to get access to this bean from your application is to "autowire" the bean.

    @Autowired
    +private InteractiveQueryService interactiveQueryService;

    Once you gain access to this bean, then you can query for the particular state-store that you are interested. See below.

    ReadOnlyKeyValueStore<Object, Object> keyValueStore =
    +						interactiveQueryService.getQueryableStoreType("my-store", QueryableStoreTypes.keyValueStore());

    If there are multiple instances of the kafka streams application running, then before you can query them interactively, you need to identify which application instance hosts the key. +InteractiveQueryService API provides methods for identifying the host information.

    In order for this to work, you must configure the property application.server as below:

    spring.cloud.stream.kafka.streams.binder.configuration.application.server: <server>:<port>

    Here are some code snippets:

    org.apache.kafka.streams.state.HostInfo hostInfo = interactiveQueryService.getHostInfo("store-name",
    +						key, keySerializer);
    +
    +if (interactiveQueryService.getCurrentHostInfo().equals(hostInfo)) {
    +
    +    //query from the store that is locally available
    +}
    +else {
    +    //query from the remote host
    +}

    40.10 Accessing the underlying KafkaStreams object

    StreamBuilderFactoryBean from spring-kafka that is responsible for constructing the KafkaStreams object can be accessed programmatically. +Each StreamBuilderFactoryBean is registered as stream-builder and appended with the StreamListener method name. +If your StreamListener method is named as process for example, the stream builder bean is named as stream-builder-process. +Since this is a factory bean, it should be accessed by prepending an ampersand (&) when accessing it programmatically. +Following is an example and it assumes the StreamListener method is named as process

    StreamsBuilderFactoryBean streamsBuilderFactoryBean = context.getBean("&stream-builder-process", StreamsBuilderFactoryBean.class);
    +			KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams();

    40.11 State Cleanup

    By default, the Kafkastreams.cleanup() method is called when the binding is stopped. +See the Spring Kafka documentation. +To modify this behavior simply add a single CleanupConfig @Bean (configured to clean up on start, stop, or neither) to the application context; the bean will be detected and wired into the factory bean.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__appendix_compendium_of_configuration_properties.html b/Greenwich.SR5/multi/multi__appendix_compendium_of_configuration_properties.html new file mode 100644 index 00000000..0327bc04 --- /dev/null +++ b/Greenwich.SR5/multi/multi__appendix_compendium_of_configuration_properties.html @@ -0,0 +1,3 @@ + + + Part XIX. Appendix: Compendium of Configuration Properties

    Part XIX. Appendix: Compendium of Configuration Properties

    Name

    Default

    Description

    aws.paramstore.default-context

    application

     

    aws.paramstore.enabled

    true

    Is AWS Parameter Store support enabled.

    aws.paramstore.fail-fast

    true

    Throw exceptions during config lookup if true, otherwise, log warnings.

    aws.paramstore.name

     

    Alternative to spring.application.name to use in looking up values in AWS Parameter Store.

    aws.paramstore.prefix

    /config

    Prefix indicating first level for every property. Value must start with a forward slash followed by a valid path segment or be empty. Defaults to "/config".

    aws.paramstore.profile-separator

    _

     

    cloud.aws.credentials.access-key

     

    The access key to be used with a static provider.

    cloud.aws.credentials.instance-profile

    true

    Configures an instance profile credentials provider with no further configuration.

    cloud.aws.credentials.profile-name

     

    The AWS profile name.

    cloud.aws.credentials.profile-path

     

    The AWS profile path.

    cloud.aws.credentials.secret-key

     

    The secret key to be used with a static provider.

    cloud.aws.credentials.use-default-aws-credentials-chain

    false

    Use the DefaultAWSCredentials Chain instead of configuring a custom credentials chain.

    cloud.aws.loader.core-pool-size

    1

    The core pool size of the Task Executor used for parallel S3 interaction. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setCorePoolSize(int)

    cloud.aws.loader.max-pool-size

     

    The maximum pool size of the Task Executor used for parallel S3 interaction. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize(int)

    cloud.aws.loader.queue-capacity

     

    The maximum queue capacity for backed up S3 requests. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setQueueCapacity(int)

    cloud.aws.region.auto

    true

    Enables automatic region detection based on the EC2 meta data service.

    cloud.aws.region.static

      

    cloud.aws.stack.auto

    true

    Enables the automatic stack name detection for the application.

    cloud.aws.stack.name

    myStackName

    The name of the manually configured stack name that will be used to retrieve the resources.

    encrypt.fail-on-error

    true

    Flag to say that a process should fail if there is an encryption or decryption error.

    encrypt.key

     

    A symmetric key. As a stronger alternative, consider using a keystore.

    encrypt.key-store.alias

     

    Alias for a key in the store.

    encrypt.key-store.location

     

    Location of the key store file, e.g. classpath:/keystore.jks.

    encrypt.key-store.password

     

    Password that locks the keystore.

    encrypt.key-store.secret

     

    Secret protecting the key (defaults to the same as the password).

    encrypt.key-store.type

    jks

    The KeyStore type. Defaults to jks.

    encrypt.rsa.algorithm

     

    The RSA algorithm to use (DEFAULT or OEAP). Once it is set, do not change it (or existing ciphers will not be decryptable).

    encrypt.rsa.salt

    deadbeef

    Salt for the random secret used to encrypt cipher text. Once it is set, do not change it (or existing ciphers will not be decryptable).

    encrypt.rsa.strong

    false

    Flag to indicate that "strong" AES encryption should be used internally. If true, then the GCM algorithm is applied to the AES encrypted bytes. Default is false (in which case "standard" CBC is used instead). Once it is set, do not change it (or existing ciphers will not be decryptable).

    encrypt.salt

    deadbeef

    A salt for the symmetric key, in the form of a hex-encoded byte array. As a stronger alternative, consider using a keystore.

    endpoints.zookeeper.enabled

    true

    Enable the /zookeeper endpoint to inspect the state of zookeeper.

    eureka.client.healthcheck.enabled

    true

    Enables the Eureka health check handler.

    health.config.enabled

    false

    Flag to indicate that the config server health indicator should be installed.

    health.config.time-to-live

    0

    Time to live for cached result, in milliseconds. Default 300000 (5 min).

    hystrix.metrics.enabled

    true

    Enable Hystrix metrics polling. Defaults to true.

    hystrix.metrics.polling-interval-ms

    2000

    Interval between subsequent polling of metrics. Defaults to 2000 ms.

    hystrix.shareSecurityContext

    false

    Enables auto-configuration of the Hystrix concurrency strategy plugin hook who will transfer the SecurityContext from your main thread to the one used by the Hystrix command.

    management.endpoint.bindings.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.bindings.enabled

    true

    Whether to enable the bindings endpoint.

    management.endpoint.bus-env.enabled

    true

    Whether to enable the bus-env endpoint.

    management.endpoint.bus-refresh.enabled

    true

    Whether to enable the bus-refresh endpoint.

    management.endpoint.channels.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.channels.enabled

    true

    Whether to enable the channels endpoint.

    management.endpoint.consul.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.consul.enabled

    true

    Whether to enable the consul endpoint.

    management.endpoint.env.post.enabled

    true

    Enables writable environment endpoint.

    management.endpoint.features.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.features.enabled

    true

    Whether to enable the features endpoint.

    management.endpoint.gateway.enabled

    true

    Whether to enable the gateway endpoint.

    management.endpoint.hystrix.config

     

    Hystrix settings. These are traditionally set using servlet parameters. Refer to the documentation of Hystrix for more details.

    management.endpoint.hystrix.stream.enabled

    true

    Whether to enable the hystrix.stream endpoint.

    management.endpoint.pause.enabled

    true

    Enable the /pause endpoint (to send Lifecycle.stop()).

    management.endpoint.refresh.enabled

    true

    Enable the /refresh endpoint to refresh configuration and re-initialize refresh scoped beans.

    management.endpoint.restart.enabled

    true

    Enable the /restart endpoint to restart the application context.

    management.endpoint.resume.enabled

    true

    Enable the /resume endpoint (to send Lifecycle.start()).

    management.endpoint.service-registry.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.service-registry.enabled

    true

    Whether to enable the service-registry endpoint.

    management.health.binders.enabled

    true

    Allows to enable/disable binder’s' health indicators. If you want to disable health indicator completely, then set it to false.

    management.health.refresh.enabled

    true

    Enable the health endpoint for the refresh scope.

    management.health.zookeeper.enabled

    true

    Enable the health endpoint for zookeeper.

    management.metrics.binders.hystrix.enabled

    true

    Enables creation of OK Http Client factory beans.

    management.metrics.export.cloudwatch.batch-size

      

    management.metrics.export.cloudwatch.connect-timeout

      

    management.metrics.export.cloudwatch.enabled

    true

    Enables cloud watch metrics.

    management.metrics.export.cloudwatch.namespace

     

    Cloud watch namespace.

    management.metrics.export.cloudwatch.num-threads

      

    management.metrics.export.cloudwatch.read-timeout

      

    management.metrics.export.cloudwatch.step

      

    maven.checksum-policy

      

    maven.connect-timeout

      

    maven.enable-repository-listener

      

    maven.local-repository

      

    maven.offline

      

    maven.proxy

      

    maven.remote-repositories

      

    maven.request-timeout

      

    maven.resolve-pom

      

    maven.update-policy

      

    proxy.auth.load-balanced

    false

     

    proxy.auth.routes

     

    Authentication strategy per route.

    ribbon.eager-load.clients

      

    ribbon.eager-load.enabled

    false

     

    ribbon.http.client.enabled

    false

    Deprecated property to enable Ribbon RestClient.

    ribbon.okhttp.enabled

    false

    Enables the use of the OK HTTP Client with Ribbon.

    ribbon.restclient.enabled

    false

    Enables the use of the deprecated Ribbon RestClient.

    ribbon.secure-ports

      

    spring.cloud.bus.ack.destination-service

     

    Service that wants to listen to acks. By default null (meaning all services).

    spring.cloud.bus.ack.enabled

    true

    Flag to switch off acks (default on).

    spring.cloud.bus.destination

    springCloudBus

    Name of Spring Cloud Stream destination for messages.

    spring.cloud.bus.enabled

    true

    Flag to indicate that the bus is enabled.

    spring.cloud.bus.env.enabled

    true

    Flag to switch off environment change events (default on).

    spring.cloud.bus.id

    application

    The identifier for this application instance.

    spring.cloud.bus.refresh.enabled

    true

    Flag to switch off refresh events (default on).

    spring.cloud.bus.trace.enabled

    false

    Flag to switch on tracing of acks (default off).

    spring.cloud.cloudfoundry.discovery.default-server-port

    80

    Port to use when no port is defined by ribbon.

    spring.cloud.cloudfoundry.discovery.enabled

    true

    Flag to indicate that discovery is enabled.

    spring.cloud.cloudfoundry.discovery.heartbeat-frequency

    5000

    Frequency in milliseconds of poll for heart beat. The client will poll on this frequency and broadcast a list of service ids.

    spring.cloud.cloudfoundry.discovery.order

    0

    Order of the discovery client used by CompositeDiscoveryClient for sorting available clients.

    spring.cloud.cloudfoundry.org

     

    Organization name to initially target.

    spring.cloud.cloudfoundry.password

     

    Password for user to authenticate and obtain token.

    spring.cloud.cloudfoundry.skip-ssl-validation

    false

     

    spring.cloud.cloudfoundry.space

     

    Space name to initially target.

    spring.cloud.cloudfoundry.url

     

    URL of Cloud Foundry API (Cloud Controller).

    spring.cloud.cloudfoundry.username

     

    Username to authenticate (usually an email address).

    spring.cloud.compatibility-verifier.compatible-boot-versions

    2.1.x

    Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don’t want to specify a concrete value. Example: {@code 3.4.x}

    spring.cloud.compatibility-verifier.enabled

    false

    Enables creation of Spring Cloud compatibility verification.

    spring.cloud.config.allow-override

    true

    Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true.

    spring.cloud.config.discovery.enabled

    false

    Flag to indicate that config server discovery is enabled (config server URL will be looked up via discovery).

    spring.cloud.config.discovery.service-id

    configserver

    Service id to locate config server.

    spring.cloud.config.enabled

    true

    Flag to say that remote configuration is enabled. Default true;

    spring.cloud.config.fail-fast

    false

    Flag to indicate that failure to connect to the server is fatal (default false).

    spring.cloud.config.headers

     

    Additional headers used to create the client request.

    spring.cloud.config.label

     

    The label name to use to pull remote configuration properties. The default is set on the server (generally "master" for a git based server).

    spring.cloud.config.name

     

    Name of application used to fetch remote properties.

    spring.cloud.config.override-none

    false

    Flag to indicate that when {@link #setAllowOverride(boolean) allowOverride} is true, external properties should take lowest priority and should not override any existing property sources (including local config files). Default false.

    spring.cloud.config.override-system-properties

    true

    Flag to indicate that the external properties should override system properties. Default true.

    spring.cloud.config.password

     

    The password to use (HTTP Basic) when contacting the remote server.

    spring.cloud.config.profile

    default

    The default profile to use when fetching remote configuration (comma-separated). Default is "default".

    spring.cloud.config.request-connect-timeout

    0

    timeout on waiting to connect to the Config Server.

    spring.cloud.config.request-read-timeout

    0

    timeout on waiting to read data from the Config Server.

    spring.cloud.config.retry.initial-interval

    1000

    Initial retry interval in milliseconds.

    spring.cloud.config.retry.max-attempts

    6

    Maximum number of attempts.

    spring.cloud.config.retry.max-interval

    2000

    Maximum interval for backoff.

    spring.cloud.config.retry.multiplier

    1.1

    Multiplier for next interval.

    spring.cloud.config.send-state

    true

    Flag to indicate whether to send state. Default true.

    spring.cloud.config.server.accept-empty

    true

    Flag to indicate that If HTTP 404 needs to be sent if Application is not Found.

    spring.cloud.config.server.bootstrap

    false

    Flag indicating that the config server should initialize its own Environment with properties from the remote repository. Off by default because it delays startup but can be useful when embedding the server in another application.

    spring.cloud.config.server.credhub.ca-cert-files

      

    spring.cloud.config.server.credhub.connection-timeout

      

    spring.cloud.config.server.credhub.oauth2.registration-id

      

    spring.cloud.config.server.credhub.order

      

    spring.cloud.config.server.credhub.read-timeout

      

    spring.cloud.config.server.credhub.url

      

    spring.cloud.config.server.default-application-name

    application

    Default application name when incoming requests do not have a specific one.

    spring.cloud.config.server.default-label

     

    Default repository label when incoming requests do not have a specific label.

    spring.cloud.config.server.default-profile

    default

    Default application profile when incoming requests do not have a specific one.

    spring.cloud.config.server.encrypt.enabled

    true

    Enable decryption of environment properties before sending to client.

    spring.cloud.config.server.git.basedir

     

    Base directory for local working copy of repository.

    spring.cloud.config.server.git.clone-on-start

    false

    Flag to indicate that the repository should be cloned on startup (not on demand). Generally leads to slower startup but faster first query.

    spring.cloud.config.server.git.default-label

     

    The default label to be used with the remote repository.

    spring.cloud.config.server.git.delete-untracked-branches

    false

    Flag to indicate that the branch should be deleted locally if it’s origin tracked branch was removed.

    spring.cloud.config.server.git.force-pull

    false

    Flag to indicate that the repository should force pull. If true discard any local changes and take from remote repository.

    spring.cloud.config.server.git.host-key

     

    Valid SSH host key. Must be set if hostKeyAlgorithm is also set.

    spring.cloud.config.server.git.host-key-algorithm

     

    One of ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521. Must be set if hostKey is also set.

    spring.cloud.config.server.git.ignore-local-ssh-settings

    false

    If true, use property-based instead of file-based SSH config.

    spring.cloud.config.server.git.known-hosts-file

     

    Location of custom .known_hosts file.

    spring.cloud.config.server.git.order

     

    The order of the environment repository.

    spring.cloud.config.server.git.passphrase

     

    Passphrase for unlocking your ssh private key.

    spring.cloud.config.server.git.password

     

    Password for authentication with remote repository.

    spring.cloud.config.server.git.preferred-authentications

     

    Override server authentication method order. This should allow for evading login prompts if server has keyboard-interactive authentication before the publickey method.

    spring.cloud.config.server.git.private-key

     

    Valid SSH private key. Must be set if ignoreLocalSshSettings is true and Git URI is SSH format.

    spring.cloud.config.server.git.proxy

     

    HTTP proxy configuration.

    spring.cloud.config.server.git.refresh-rate

    0

    Time (in seconds) between refresh of the git repository.

    spring.cloud.config.server.git.repos

     

    Map of repository identifier to location and other properties.

    spring.cloud.config.server.git.search-paths

     

    Search paths to use within local working copy. By default searches only the root.

    spring.cloud.config.server.git.skip-ssl-validation

    false

    Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served over an HTTPS connection.

    spring.cloud.config.server.git.strict-host-key-checking

    true

    If false, ignore errors with host key.

    spring.cloud.config.server.git.timeout

    5

    Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable), defaults to 5 seconds.

    spring.cloud.config.server.git.uri

     

    URI of remote repository.

    spring.cloud.config.server.git.username

     

    Username for authentication with remote repository.

    spring.cloud.config.server.health.repositories

      

    spring.cloud.config.server.jdbc.order

    0

     

    spring.cloud.config.server.jdbc.sql

    SELECT KEY, VALUE from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=?

    SQL used to query database for keys and values.

    spring.cloud.config.server.native.add-label-locations

    true

    Flag to determine whether label locations should be added.

    spring.cloud.config.server.native.default-label

    master

     

    spring.cloud.config.server.native.fail-on-error

    false

    Flag to determine how to handle exceptions during decryption (default false).

    spring.cloud.config.server.native.order

      

    spring.cloud.config.server.native.search-locations

    []

    Locations to search for configuration files. Defaults to the same as a Spring Boot app so [classpath:/,classpath:/config/,file:./,file:./config/].

    spring.cloud.config.server.native.version

     

    Version string to be reported for native repository.

    spring.cloud.config.server.overrides

     

    Extra map for a property source to be sent to all clients unconditionally.

    spring.cloud.config.server.prefix

     

    Prefix for configuration resource paths (default is empty). Useful when embedding in another application when you don’t want to change the context path or servlet path.

    spring.cloud.config.server.strip-document-from-yaml

    true

    Flag to indicate that YAML documents that are text or collections (not a map) should be returned in "native" form.

    spring.cloud.config.server.svn.basedir

     

    Base directory for local working copy of repository.

    spring.cloud.config.server.svn.default-label

     

    The default label to be used with the remote repository.

    spring.cloud.config.server.svn.order

     

    The order of the environment repository.

    spring.cloud.config.server.svn.passphrase

     

    Passphrase for unlocking your ssh private key.

    spring.cloud.config.server.svn.password

     

    Password for authentication with remote repository.

    spring.cloud.config.server.svn.search-paths

     

    Search paths to use within local working copy. By default searches only the root.

    spring.cloud.config.server.svn.strict-host-key-checking

    true

    Reject incoming SSH host keys from remote servers not in the known host list.

    spring.cloud.config.server.svn.uri

     

    URI of remote repository.

    spring.cloud.config.server.svn.username

     

    Username for authentication with remote repository.

    spring.cloud.config.server.vault.backend

    secret

    Vault backend. Defaults to secret.

    spring.cloud.config.server.vault.default-key

    application

    The key in vault shared by all applications. Defaults to application. Set to empty to disable.

    spring.cloud.config.server.vault.host

    127.0.0.1

    Vault host. Defaults to 127.0.0.1.

    spring.cloud.config.server.vault.kv-version

    1

    Value to indicate which version of Vault kv backend is used. Defaults to 1.

    spring.cloud.config.server.vault.namespace

     

    The value of the Vault X-Vault-Namespace header. Defaults to null. This a Vault Enterprise feature only.

    spring.cloud.config.server.vault.order

      

    spring.cloud.config.server.vault.port

    8200

    Vault port. Defaults to 8200.

    spring.cloud.config.server.vault.profile-separator

    ,

    Vault profile separator. Defaults to comma.

    spring.cloud.config.server.vault.proxy

     

    HTTP proxy configuration.

    spring.cloud.config.server.vault.scheme

    http

    Vault scheme. Defaults to http.

    spring.cloud.config.server.vault.skip-ssl-validation

    false

    Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served over an HTTPS connection.

    spring.cloud.config.server.vault.timeout

    5

    Timeout (in seconds) for obtaining HTTP connection, defaults to 5 seconds.

    spring.cloud.config.token

     

    Security Token passed thru to underlying environment repository.

    spring.cloud.config.uri

    [http://localhost:8888]

    The URI of the remote server (default http://localhost:8888).

    spring.cloud.config.username

     

    The username to use (HTTP Basic) when contacting the remote server.

    spring.cloud.consul.config.acl-token

      

    spring.cloud.consul.config.data-key

    data

    If format is Format.PROPERTIES or Format.YAML then the following field is used as key to look up consul for configuration.

    spring.cloud.consul.config.default-context

    application

     

    spring.cloud.consul.config.enabled

    true

     

    spring.cloud.consul.config.fail-fast

    true

    Throw exceptions during config lookup if true, otherwise, log warnings.

    spring.cloud.consul.config.format

      

    spring.cloud.consul.config.name

     

    Alternative to spring.application.name to use in looking up values in consul KV.

    spring.cloud.consul.config.prefix

    config

     

    spring.cloud.consul.config.profile-separator

    ,

     

    spring.cloud.consul.config.watch.delay

    1000

    The value of the fixed delay for the watch in millis. Defaults to 1000.

    spring.cloud.consul.config.watch.enabled

    true

    If the watch is enabled. Defaults to true.

    spring.cloud.consul.config.watch.wait-time

    55

    The number of seconds to wait (or block) for watch query, defaults to 55. Needs to be less than default ConsulClient (defaults to 60). To increase ConsulClient timeout create a ConsulClient bean with a custom ConsulRawClient with a custom HttpClient.

    spring.cloud.consul.discovery.acl-token

      

    spring.cloud.consul.discovery.catalog-services-watch-delay

    1000

    The delay between calls to watch consul catalog in millis, default is 1000.

    spring.cloud.consul.discovery.catalog-services-watch-timeout

    2

    The number of seconds to block while watching consul catalog, default is 2.

    spring.cloud.consul.discovery.datacenters

     

    Map of serviceId’s → datacenter to query for in server list. This allows looking up services in another datacenters.

    spring.cloud.consul.discovery.default-query-tag

     

    Tag to query for in service list if one is not listed in serverListQueryTags.

    spring.cloud.consul.discovery.default-zone-metadata-name

    zone

    Service instance zone comes from metadata. This allows changing the metadata tag name.

    spring.cloud.consul.discovery.deregister

    true

    Disable automatic de-registration of service in consul.

    spring.cloud.consul.discovery.enabled

    true

    Is service discovery enabled?

    spring.cloud.consul.discovery.fail-fast

    true

    Throw exceptions during service registration if true, otherwise, log warnings (defaults to true).

    spring.cloud.consul.discovery.health-check-critical-timeout

     

    Timeout to deregister services critical for longer than timeout (e.g. 30m). Requires consul version 7.x or higher.

    spring.cloud.consul.discovery.health-check-headers

     

    Headers to be applied to the Health Check calls.

    spring.cloud.consul.discovery.health-check-interval

    10s

    How often to perform the health check (e.g. 10s), defaults to 10s.

    spring.cloud.consul.discovery.health-check-path

    /actuator/health

    Alternate server path to invoke for health checking.

    spring.cloud.consul.discovery.health-check-timeout

     

    Timeout for health check (e.g. 10s).

    spring.cloud.consul.discovery.health-check-tls-skip-verify

     

    Skips certificate verification during service checks if true, otherwise runs certificate verification.

    spring.cloud.consul.discovery.health-check-url

     

    Custom health check url to override default.

    spring.cloud.consul.discovery.heartbeat.enabled

    false

     

    spring.cloud.consul.discovery.heartbeat.interval-ratio

      

    spring.cloud.consul.discovery.heartbeat.ttl-unit

    s

     

    spring.cloud.consul.discovery.heartbeat.ttl-value

    30

     

    spring.cloud.consul.discovery.hostname

     

    Hostname to use when accessing server.

    spring.cloud.consul.discovery.instance-group

     

    Service instance group.

    spring.cloud.consul.discovery.instance-id

     

    Unique service instance id.

    spring.cloud.consul.discovery.instance-zone

     

    Service instance zone.

    spring.cloud.consul.discovery.ip-address

     

    IP address to use when accessing service (must also set preferIpAddress to use).

    spring.cloud.consul.discovery.lifecycle.enabled

    true

     

    spring.cloud.consul.discovery.management-port

     

    Port to register the management service under (defaults to management port).

    spring.cloud.consul.discovery.management-suffix

    management

    Suffix to use when registering management service.

    spring.cloud.consul.discovery.management-tags

     

    Tags to use when registering management service.

    spring.cloud.consul.discovery.order

    0

    Order of the discovery client used by CompositeDiscoveryClient for sorting available clients.

    spring.cloud.consul.discovery.port

     

    Port to register the service under (defaults to listening port).

    spring.cloud.consul.discovery.prefer-agent-address

    false

    Source of how we will determine the address to use.

    spring.cloud.consul.discovery.prefer-ip-address

    false

    Use ip address rather than hostname during registration.

    spring.cloud.consul.discovery.query-passing

    false

    Add the 'passing` parameter to /v1/health/service/serviceName. This pushes health check passing to the server.

    spring.cloud.consul.discovery.register

    true

    Register as a service in consul.

    spring.cloud.consul.discovery.register-health-check

    true

    Register health check in consul. Useful during development of a service.

    spring.cloud.consul.discovery.scheme

    http

    Whether to register an http or https service.

    spring.cloud.consul.discovery.server-list-query-tags

     

    Map of serviceId’s → tag to query for in server list. This allows filtering services by a single tag.

    spring.cloud.consul.discovery.service-name

     

    Service name.

    spring.cloud.consul.discovery.tags

     

    Tags to use when registering service.

    spring.cloud.consul.enabled

    true

    Is spring cloud consul enabled.

    spring.cloud.consul.host

    localhost

    Consul agent hostname. Defaults to 'localhost'.

    spring.cloud.consul.port

    8500

    Consul agent port. Defaults to '8500'.

    spring.cloud.consul.retry.initial-interval

    1000

    Initial retry interval in milliseconds.

    spring.cloud.consul.retry.max-attempts

    6

    Maximum number of attempts.

    spring.cloud.consul.retry.max-interval

    2000

    Maximum interval for backoff.

    spring.cloud.consul.retry.multiplier

    1.1

    Multiplier for next interval.

    spring.cloud.consul.scheme

     

    Consul agent scheme (HTTP/HTTPS). If there is no scheme in address - client will use HTTP.

    spring.cloud.consul.tls.certificate-password

     

    Password to open the certificate.

    spring.cloud.consul.tls.certificate-path

     

    File path to the certificate.

    spring.cloud.consul.tls.key-store-instance-type

     

    Type of key framework to use.

    spring.cloud.consul.tls.key-store-password

     

    Password to an external keystore.

    spring.cloud.consul.tls.key-store-path

     

    Path to an external keystore.

    spring.cloud.discovery.client.cloudfoundry.order

      

    spring.cloud.discovery.client.composite-indicator.enabled

    true

    Enables discovery client composite health indicator.

    spring.cloud.discovery.client.health-indicator.enabled

    true

     

    spring.cloud.discovery.client.health-indicator.include-description

    false

     

    spring.cloud.discovery.client.simple.instances

      

    spring.cloud.discovery.client.simple.local.instance-id

     

    The unique identifier or name for the service instance.

    spring.cloud.discovery.client.simple.local.metadata

     

    Metadata for the service instance. Can be used by discovery clients to modify their behaviour per instance, e.g. when load balancing.

    spring.cloud.discovery.client.simple.local.service-id

     

    The identifier or name for the service. Multiple instances might share the same service ID.

    spring.cloud.discovery.client.simple.local.uri

     

    The URI of the service instance. Will be parsed to extract the scheme, host, and port.

    spring.cloud.discovery.client.simple.order

      

    spring.cloud.discovery.enabled

    true

    Enables discovery client health indicators.

    spring.cloud.features.enabled

    true

    Enables the features endpoint.

    spring.cloud.function.compile

     

    Configuration for function bodies, which will be compiled. The key in the map is the function name and the value is a map containing a key "lambda" which is the body to compile, and optionally a "type" (defaults to "function"). Can also contain "inputType" and "outputType" in case it is ambiguous.

    spring.cloud.function.definition

     

    Name (e.g., 'foo') or composition instruction (e.g., 'foo|bar') used to resolve default function especially for cases where there is more then once function available in catalog.

    spring.cloud.function.imports

     

    Configuration for a set of files containing function bodies, which will be imported and compiled. The key in the map is the function name and the value is another map, containing a "location" of the file to compile and (optionally) a "type" (defaults to "function").

    spring.cloud.function.scan.packages

    functions

    Triggers scanning within the specified base packages for any class that is assignable to java.util.function.Function. For each detected Function class, a bean instance will be added to the context.

    spring.cloud.function.task.consumer

      

    spring.cloud.function.task.function

      

    spring.cloud.function.task.supplier

      

    spring.cloud.function.web.path

     

    Path to web resources for functions (should start with / if not empty).

    spring.cloud.function.web.supplier.auto-startup

    true

     

    spring.cloud.function.web.supplier.debug

    true

     

    spring.cloud.function.web.supplier.enabled

    false

     

    spring.cloud.function.web.supplier.headers

      

    spring.cloud.function.web.supplier.name

      

    spring.cloud.function.web.supplier.template-url

      

    spring.cloud.gateway.default-filters

     

    List of filter definitions that are applied to every route.

    spring.cloud.gateway.discovery.locator.enabled

    false

    Flag that enables DiscoveryClient gateway integration.

    spring.cloud.gateway.discovery.locator.filters

      

    spring.cloud.gateway.discovery.locator.include-expression

    true

    SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true.

    spring.cloud.gateway.discovery.locator.lower-case-service-id

    false

    Option to lower case serviceId in predicates and filters, defaults to false. Useful with eureka when it automatically uppercases serviceId. so MYSERIVCE, would match /myservice/**

    spring.cloud.gateway.discovery.locator.predicates

      

    spring.cloud.gateway.discovery.locator.route-id-prefix

     

    The prefix for the routeId, defaults to discoveryClient.getClass().getSimpleName() + "_". Service Id will be appended to create the routeId.

    spring.cloud.gateway.discovery.locator.url-expression

    'lb://'+serviceId

    SpEL expression that create the uri for each route, defaults to: 'lb://'+serviceId.

    spring.cloud.gateway.enabled

    true

    Enables gateway functionality.

    spring.cloud.gateway.filter.remove-hop-by-hop.headers

      

    spring.cloud.gateway.filter.remove-hop-by-hop.order

      

    spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key

    true

    Switch to deny requests if the Key Resolver returns an empty key, defaults to true.

    spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code

     

    HttpStatus to return when denyEmptyKey is true, defaults to FORBIDDEN.

    spring.cloud.gateway.filter.secure-headers.content-security-policy

    default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'

     

    spring.cloud.gateway.filter.secure-headers.content-type-options

    nosniff

     

    spring.cloud.gateway.filter.secure-headers.disable

      

    spring.cloud.gateway.filter.secure-headers.download-options

    noopen

     

    spring.cloud.gateway.filter.secure-headers.frame-options

    DENY

     

    spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies

    none

     

    spring.cloud.gateway.filter.secure-headers.referrer-policy

    no-referrer

     

    spring.cloud.gateway.filter.secure-headers.strict-transport-security

    max-age=631138519

     

    spring.cloud.gateway.filter.secure-headers.xss-protection-header

    1 ; mode=block

     

    spring.cloud.gateway.forwarded.enabled

    true

    Enables the ForwardedHeadersFilter.

    spring.cloud.gateway.globalcors.cors-configurations

      

    spring.cloud.gateway.httpclient.connect-timeout

     

    The connect timeout in millis, the default is 45s.

    spring.cloud.gateway.httpclient.max-header-size

     

    The max response header size.

    spring.cloud.gateway.httpclient.pool.acquire-timeout

     

    Only for type FIXED, the maximum time in millis to wait for aquiring.

    spring.cloud.gateway.httpclient.pool.max-connections

     

    Only for type FIXED, the maximum number of connections before starting pending acquisition on existing ones.

    spring.cloud.gateway.httpclient.pool.name

    proxy

    The channel pool map name, defaults to proxy.

    spring.cloud.gateway.httpclient.pool.type

     

    Type of pool for HttpClient to use, defaults to ELASTIC.

    spring.cloud.gateway.httpclient.proxy.host

     

    Hostname for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.proxy.non-proxy-hosts-pattern

     

    Regular expression (Java) for a configured list of hosts. that should be reached directly, bypassing the proxy

    spring.cloud.gateway.httpclient.proxy.password

     

    Password for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.proxy.port

     

    Port for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.proxy.username

     

    Username for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.response-timeout

     

    The response timeout.

    spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout

    3000ms

    SSL close_notify flush timeout. Default to 3000 ms.

    spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout-millis

      

    spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout

     

    SSL close_notify read timeout. Default to 0 ms.

    spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout-millis

      

    spring.cloud.gateway.httpclient.ssl.default-configuration-type

     

    The default ssl configuration type. Defaults to TCP.

    spring.cloud.gateway.httpclient.ssl.handshake-timeout

    10000ms

    SSL handshake timeout. Default to 10000 ms

    spring.cloud.gateway.httpclient.ssl.handshake-timeout-millis

      

    spring.cloud.gateway.httpclient.ssl.trusted-x509-certificates

     

    Trusted certificates for verifying the remote endpoint’s certificate.

    spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager

    false

    Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production.

    spring.cloud.gateway.httpclient.wiretap

    false

    Enables wiretap debugging for Netty HttpClient.

    spring.cloud.gateway.httpserver.wiretap

    false

    Enables wiretap debugging for Netty HttpServer.

    spring.cloud.gateway.loadbalancer.use404

    false

     

    spring.cloud.gateway.metrics.enabled

    true

    Enables the collection of metrics data.

    spring.cloud.gateway.proxy.headers

     

    Fixed header values that will be added to all downstream requests.

    spring.cloud.gateway.proxy.sensitive

     

    A set of sensitive header names that will not be sent downstream by default.

    spring.cloud.gateway.redis-rate-limiter.burst-capacity-header

    X-RateLimit-Burst-Capacity

    The name of the header that returns the burst capacity configuration.

    spring.cloud.gateway.redis-rate-limiter.config

      

    spring.cloud.gateway.redis-rate-limiter.include-headers

    true

    Whether or not to include headers containing rate limiter information, defaults to true.

    spring.cloud.gateway.redis-rate-limiter.remaining-header

    X-RateLimit-Remaining

    The name of the header that returns number of remaining requests during the current second.

    spring.cloud.gateway.redis-rate-limiter.replenish-rate-header

    X-RateLimit-Replenish-Rate

    The name of the header that returns the replenish rate configuration.

    spring.cloud.gateway.routes

     

    List of Routes.

    spring.cloud.gateway.streaming-media-types

      

    spring.cloud.gateway.x-forwarded.enabled

    true

    If the XForwardedHeadersFilter is enabled.

    spring.cloud.gateway.x-forwarded.for-append

    true

    If appending X-Forwarded-For as a list is enabled.

    spring.cloud.gateway.x-forwarded.for-enabled

    true

    If X-Forwarded-For is enabled.

    spring.cloud.gateway.x-forwarded.host-append

    true

    If appending X-Forwarded-Host as a list is enabled.

    spring.cloud.gateway.x-forwarded.host-enabled

    true

    If X-Forwarded-Host is enabled.

    spring.cloud.gateway.x-forwarded.order

    0

    The order of the XForwardedHeadersFilter.

    spring.cloud.gateway.x-forwarded.port-append

    true

    If appending X-Forwarded-Port as a list is enabled.

    spring.cloud.gateway.x-forwarded.port-enabled

    true

    If X-Forwarded-Port is enabled.

    spring.cloud.gateway.x-forwarded.prefix-append

    true

    If appending X-Forwarded-Prefix as a list is enabled.

    spring.cloud.gateway.x-forwarded.prefix-enabled

    true

    If X-Forwarded-Prefix is enabled.

    spring.cloud.gateway.x-forwarded.proto-append

    true

    If appending X-Forwarded-Proto as a list is enabled.

    spring.cloud.gateway.x-forwarded.proto-enabled

    true

    If X-Forwarded-Proto is enabled.

    spring.cloud.gcp.config.credentials.encoded-key

      

    spring.cloud.gcp.config.credentials.location

      

    spring.cloud.gcp.config.credentials.scopes

      

    spring.cloud.gcp.config.enabled

    false

    Enables Spring Cloud GCP Config.

    spring.cloud.gcp.config.name

     

    Name of the application.

    spring.cloud.gcp.config.profile

     

    Comma-delimited string of profiles under which the app is running. Gets its default value from the {@code spring.profiles.active} property, falling back on the {@code spring.profiles.default} property.

    spring.cloud.gcp.config.project-id

     

    Overrides the GCP project ID specified in the Core module.

    spring.cloud.gcp.config.timeout-millis

    60000

    Timeout for Google Runtime Configuration API calls.

    spring.cloud.gcp.credentials.encoded-key

      

    spring.cloud.gcp.credentials.location

      

    spring.cloud.gcp.credentials.scopes

      

    spring.cloud.gcp.datastore.credentials.encoded-key

      

    spring.cloud.gcp.datastore.credentials.location

      

    spring.cloud.gcp.datastore.credentials.scopes

      

    spring.cloud.gcp.datastore.namespace

      

    spring.cloud.gcp.datastore.project-id

      

    spring.cloud.gcp.logging.enabled

    true

    Auto-configure Google Cloud Stackdriver logging for Spring MVC.

    spring.cloud.gcp.project-id

     

    GCP project ID where services are running.

    spring.cloud.gcp.pubsub.credentials.encoded-key

      

    spring.cloud.gcp.pubsub.credentials.location

      

    spring.cloud.gcp.pubsub.credentials.scopes

      

    spring.cloud.gcp.pubsub.emulator-host

     

    The host and port of the local running emulator. If provided, this will setup the client to connect against a running pub/sub emulator.

    spring.cloud.gcp.pubsub.enabled

    true

    Auto-configure Google Cloud Pub/Sub components.

    spring.cloud.gcp.pubsub.project-id

     

    Overrides the GCP project ID specified in the Core module.

    spring.cloud.gcp.pubsub.publisher.batching.delay-threshold-seconds

     

    The delay threshold to use for batching. After this amount of time has elapsed (counting from the first element added), the elements will be wrapped up in a batch and sent.

    spring.cloud.gcp.pubsub.publisher.batching.element-count-threshold

     

    The element count threshold to use for batching.

    spring.cloud.gcp.pubsub.publisher.batching.enabled

     

    Enables batching if true.

    spring.cloud.gcp.pubsub.publisher.batching.flow-control.limit-exceeded-behavior

     

    The behavior when the specified limits are exceeded.

    spring.cloud.gcp.pubsub.publisher.batching.flow-control.max-outstanding-element-count

     

    Maximum number of outstanding elements to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.publisher.batching.flow-control.max-outstanding-request-bytes

     

    Maximum number of outstanding bytes to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.publisher.batching.request-byte-threshold

     

    The request byte threshold to use for batching.

    spring.cloud.gcp.pubsub.publisher.executor-threads

    4

    Number of threads used by every publisher.

    spring.cloud.gcp.pubsub.publisher.retry.initial-retry-delay-seconds

     

    InitialRetryDelay controls the delay before the first retry. Subsequent retries will use this value adjusted according to the RetryDelayMultiplier.

    spring.cloud.gcp.pubsub.publisher.retry.initial-rpc-timeout-seconds

     

    InitialRpcTimeout controls the timeout for the initial RPC. Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier.

    spring.cloud.gcp.pubsub.publisher.retry.jittered

     

    Jitter determines if the delay time should be randomized.

    spring.cloud.gcp.pubsub.publisher.retry.max-attempts

     

    MaxAttempts defines the maximum number of attempts to perform. If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout.

    spring.cloud.gcp.pubsub.publisher.retry.max-retry-delay-seconds

     

    MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier can’t increase the retry delay higher than this amount.

    spring.cloud.gcp.pubsub.publisher.retry.max-rpc-timeout-seconds

     

    MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier can’t increase the RPC timeout higher than this amount.

    spring.cloud.gcp.pubsub.publisher.retry.retry-delay-multiplier

     

    RetryDelayMultiplier controls the change in retry delay. The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call.

    spring.cloud.gcp.pubsub.publisher.retry.rpc-timeout-multiplier

     

    RpcTimeoutMultiplier controls the change in RPC timeout. The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call.

    spring.cloud.gcp.pubsub.publisher.retry.total-timeout-seconds

     

    TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. The higher the total timeout, the more retries can be attempted.

    spring.cloud.gcp.pubsub.subscriber.executor-threads

    4

    Number of threads used by every subscriber.

    spring.cloud.gcp.pubsub.subscriber.flow-control.limit-exceeded-behavior

     

    The behavior when the specified limits are exceeded.

    spring.cloud.gcp.pubsub.subscriber.flow-control.max-outstanding-element-count

     

    Maximum number of outstanding elements to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.subscriber.flow-control.max-outstanding-request-bytes

     

    Maximum number of outstanding bytes to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.subscriber.max-ack-extension-period

    0

    The optional max ack extension period in seconds for the subscriber factory.

    spring.cloud.gcp.pubsub.subscriber.max-acknowledgement-threads

    4

    Number of threads used for batch acknowledgement.

    spring.cloud.gcp.pubsub.subscriber.parallel-pull-count

     

    The optional parallel pull count setting for the subscriber factory.

    spring.cloud.gcp.pubsub.subscriber.pull-endpoint

     

    The optional pull endpoint setting for the subscriber factory.

    spring.cloud.gcp.pubsub.subscriber.retry.initial-retry-delay-seconds

     

    InitialRetryDelay controls the delay before the first retry. Subsequent retries will use this value adjusted according to the RetryDelayMultiplier.

    spring.cloud.gcp.pubsub.subscriber.retry.initial-rpc-timeout-seconds

     

    InitialRpcTimeout controls the timeout for the initial RPC. Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier.

    spring.cloud.gcp.pubsub.subscriber.retry.jittered

     

    Jitter determines if the delay time should be randomized.

    spring.cloud.gcp.pubsub.subscriber.retry.max-attempts

     

    MaxAttempts defines the maximum number of attempts to perform. If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout.

    spring.cloud.gcp.pubsub.subscriber.retry.max-retry-delay-seconds

     

    MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier can’t increase the retry delay higher than this amount.

    spring.cloud.gcp.pubsub.subscriber.retry.max-rpc-timeout-seconds

     

    MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier can’t increase the RPC timeout higher than this amount.

    spring.cloud.gcp.pubsub.subscriber.retry.retry-delay-multiplier

     

    RetryDelayMultiplier controls the change in retry delay. The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call.

    spring.cloud.gcp.pubsub.subscriber.retry.rpc-timeout-multiplier

     

    RpcTimeoutMultiplier controls the change in RPC timeout. The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call.

    spring.cloud.gcp.pubsub.subscriber.retry.total-timeout-seconds

     

    TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. The higher the total timeout, the more retries can be attempted.

    spring.cloud.gcp.security.iap.algorithm

    ES256

    Encryption algorithm used to sign the JWK token.

    spring.cloud.gcp.security.iap.audience

     

    Non-dynamic audience string to validate.

    spring.cloud.gcp.security.iap.enabled

    true

    Auto-configure Google Cloud IAP identity extraction components.

    spring.cloud.gcp.security.iap.header

    x-goog-iap-jwt-assertion

    Header from which to extract the JWK key.

    spring.cloud.gcp.security.iap.issuer

    https://cloud.google.com/iap

    JWK issuer to verify.

    spring.cloud.gcp.security.iap.registry

    https://www.gstatic.com/iap/verify/public_key-jwk

    Link to JWK public key registry.

    spring.cloud.gcp.spanner.create-interleaved-table-ddl-on-delete-cascade

    true

     

    spring.cloud.gcp.spanner.credentials.encoded-key

      

    spring.cloud.gcp.spanner.credentials.location

      

    spring.cloud.gcp.spanner.credentials.scopes

      

    spring.cloud.gcp.spanner.database

      

    spring.cloud.gcp.spanner.instance-id

      

    spring.cloud.gcp.spanner.keep-alive-interval-minutes

    -1

     

    spring.cloud.gcp.spanner.max-idle-sessions

    -1

     

    spring.cloud.gcp.spanner.max-sessions

    -1

     

    spring.cloud.gcp.spanner.min-sessions

    -1

     

    spring.cloud.gcp.spanner.num-rpc-channels

    -1

     

    spring.cloud.gcp.spanner.prefetch-chunks

    -1

     

    spring.cloud.gcp.spanner.project-id

      

    spring.cloud.gcp.spanner.write-sessions-fraction

    -1

     

    spring.cloud.gcp.sql.credentials

     

    Overrides the GCP OAuth2 credentials specified in the Core module.

    spring.cloud.gcp.sql.database-name

     

    Name of the database in the Cloud SQL instance.

    spring.cloud.gcp.sql.enabled

    true

    Auto-configure Google Cloud SQL support components.

    spring.cloud.gcp.sql.instance-connection-name

     

    Cloud SQL instance connection name. [GCP_PROJECT_ID]:[INSTANCE_REGION]:[INSTANCE_NAME].

    spring.cloud.gcp.storage.auto-create-files

      

    spring.cloud.gcp.storage.credentials.encoded-key

      

    spring.cloud.gcp.storage.credentials.location

      

    spring.cloud.gcp.storage.credentials.scopes

      

    spring.cloud.gcp.storage.enabled

    true

    Auto-configure Google Cloud Storage components.

    spring.cloud.gcp.trace.authority

     

    HTTP/2 authority the channel claims to be connecting to.

    spring.cloud.gcp.trace.compression

     

    Compression to use for the call.

    spring.cloud.gcp.trace.credentials.encoded-key

      

    spring.cloud.gcp.trace.credentials.location

      

    spring.cloud.gcp.trace.credentials.scopes

      

    spring.cloud.gcp.trace.deadline-ms

     

    Call deadline.

    spring.cloud.gcp.trace.enabled

    true

    Auto-configure Google Cloud Stackdriver tracing components.

    spring.cloud.gcp.trace.max-inbound-size

     

    Maximum size for an inbound message.

    spring.cloud.gcp.trace.max-outbound-size

     

    Maximum size for an outbound message.

    spring.cloud.gcp.trace.message-timeout

     

    Timeout in seconds before pending spans will be sent in batches to GCP Stackdriver Trace.

    spring.cloud.gcp.trace.num-executor-threads

    4

    Number of threads to be used by the Trace executor.

    spring.cloud.gcp.trace.project-id

     

    Overrides the GCP project ID specified in the Core module.

    spring.cloud.gcp.trace.wait-for-ready

     

    Waits for the channel to be ready in case of a transient failure. Defaults to failing fast in that case.

    spring.cloud.gcp.vision.credentials.encoded-key

      

    spring.cloud.gcp.vision.credentials.location

      

    spring.cloud.gcp.vision.credentials.scopes

      

    spring.cloud.gcp.vision.enabled

    true

    Auto-configure Google Cloud Vision components.

    spring.cloud.httpclientfactories.apache.enabled

    true

    Enables creation of Apache Http Client factory beans.

    spring.cloud.httpclientfactories.ok.enabled

    true

    Enables creation of OK Http Client factory beans.

    spring.cloud.hypermedia.refresh.fixed-delay

    5000

     

    spring.cloud.hypermedia.refresh.initial-delay

    10000

     

    spring.cloud.inetutils.default-hostname

    localhost

    The default hostname. Used in case of errors.

    spring.cloud.inetutils.default-ip-address

    127.0.0.1

    The default IP address. Used in case of errors.

    spring.cloud.inetutils.ignored-interfaces

     

    List of Java regular expressions for network interfaces that will be ignored.

    spring.cloud.inetutils.preferred-networks

     

    List of Java regular expressions for network addresses that will be preferred.

    spring.cloud.inetutils.timeout-seconds

    1

    Timeout, in seconds, for calculating hostname.

    spring.cloud.inetutils.use-only-site-local-interfaces

    false

    Whether to use only interfaces with site local addresses. See {@link InetAddress#isSiteLocalAddress()} for more details.

    spring.cloud.kubernetes.client.api-version

      

    spring.cloud.kubernetes.client.apiVersion

    v1

    Kubernetes API Version

    spring.cloud.kubernetes.client.ca-cert-data

      

    spring.cloud.kubernetes.client.ca-cert-file

      

    spring.cloud.kubernetes.client.caCertData

     

    Kubernetes API CACertData

    spring.cloud.kubernetes.client.caCertFile

     

    Kubernetes API CACertFile

    spring.cloud.kubernetes.client.client-cert-data

      

    spring.cloud.kubernetes.client.client-cert-file

      

    spring.cloud.kubernetes.client.client-key-algo

      

    spring.cloud.kubernetes.client.client-key-data

      

    spring.cloud.kubernetes.client.client-key-file

      

    spring.cloud.kubernetes.client.client-key-passphrase

      

    spring.cloud.kubernetes.client.clientCertData

     

    Kubernetes API ClientCertData

    spring.cloud.kubernetes.client.clientCertFile

     

    Kubernetes API ClientCertFile

    spring.cloud.kubernetes.client.clientKeyAlgo

    RSA

    Kubernetes API ClientKeyAlgo

    spring.cloud.kubernetes.client.clientKeyData

     

    Kubernetes API ClientKeyData

    spring.cloud.kubernetes.client.clientKeyFile

     

    Kubernetes API ClientKeyFile

    spring.cloud.kubernetes.client.clientKeyPassphrase

    changeit

    Kubernetes API ClientKeyPassphrase

    spring.cloud.kubernetes.client.connection-timeout

      

    spring.cloud.kubernetes.client.connectionTimeout

    10s

    Connection timeout

    spring.cloud.kubernetes.client.http-proxy

      

    spring.cloud.kubernetes.client.https-proxy

      

    spring.cloud.kubernetes.client.logging-interval

      

    spring.cloud.kubernetes.client.loggingInterval

    20s

    Logging interval

    spring.cloud.kubernetes.client.master-url

      

    spring.cloud.kubernetes.client.masterUrl

    https://kubernetes.default.svc

    Kubernetes API Master Node URL

    spring.cloud.kubernetes.client.namespace

    true

    Kubernetes Namespace

    spring.cloud.kubernetes.client.no-proxy

      

    spring.cloud.kubernetes.client.password

     

    Kubernetes API Password

    spring.cloud.kubernetes.client.proxy-password

      

    spring.cloud.kubernetes.client.proxy-username

      

    spring.cloud.kubernetes.client.request-timeout

      

    spring.cloud.kubernetes.client.requestTimeout

    10s

    Request timeout

    spring.cloud.kubernetes.client.rolling-timeout

      

    spring.cloud.kubernetes.client.rollingTimeout

    900s

    Rolling timeout

    spring.cloud.kubernetes.client.trust-certs

      

    spring.cloud.kubernetes.client.trustCerts

    false

    Kubernetes API Trust Certificates

    spring.cloud.kubernetes.client.username

     

    Kubernetes API Username

    spring.cloud.kubernetes.client.watch-reconnect-interval

      

    spring.cloud.kubernetes.client.watch-reconnect-limit

      

    spring.cloud.kubernetes.client.watchReconnectInterval

    1s

    Reconnect Interval

    spring.cloud.kubernetes.client.watchReconnectLimit

    -1

    Reconnect Interval limit retries

    spring.cloud.kubernetes.config.enable-api

    true

     

    spring.cloud.kubernetes.config.enabled

    true

    Enable the ConfigMap property source locator.

    spring.cloud.kubernetes.config.name

      

    spring.cloud.kubernetes.config.namespace

      

    spring.cloud.kubernetes.config.paths

      

    spring.cloud.kubernetes.config.sources

      

    spring.cloud.kubernetes.reload.enabled

    false

    Enables the Kubernetes configuration reload on change.

    spring.cloud.kubernetes.reload.mode

     

    Sets the detection mode for Kubernetes configuration reload.

    spring.cloud.kubernetes.reload.monitoring-config-maps

    true

    Enables monitoring on config maps to detect changes.

    spring.cloud.kubernetes.reload.monitoring-secrets

    false

    Enables monitoring on secrets to detect changes.

    spring.cloud.kubernetes.reload.period

    15000ms

    Sets the polling period to use when the detection mode is POLLING.

    spring.cloud.kubernetes.reload.strategy

     

    Sets the reload strategy for Kubernetes configuration reload on change.

    spring.cloud.kubernetes.secrets.enable-api

    false

     

    spring.cloud.kubernetes.secrets.enabled

    true

    Enable the Secrets property source locator.

    spring.cloud.kubernetes.secrets.labels

      

    spring.cloud.kubernetes.secrets.name

      

    spring.cloud.kubernetes.secrets.namespace

      

    spring.cloud.kubernetes.secrets.paths

      

    spring.cloud.loadbalancer.retry.enabled

    true

     

    spring.cloud.refresh.enabled

    true

    Enables autoconfiguration for the refresh scope and associated features.

    spring.cloud.refresh.extra-refreshable

    true

    Additional class names for beans to post process into refresh scope.

    spring.cloud.service-registry.auto-registration.enabled

    true

    Whether service auto-registration is enabled. Defaults to true.

    spring.cloud.service-registry.auto-registration.fail-fast

    false

    Whether startup fails if there is no AutoServiceRegistration. Defaults to false.

    spring.cloud.service-registry.auto-registration.register-management

    true

    Whether to register the management as a service. Defaults to true.

    spring.cloud.stream.binders

     

    Additional per-binder properties (see {@link BinderProperties}) if more then one binder of the same type is used (i.e., connect to multiple instances of RabbitMq). Here you can specify multiple binder configurations, each with different environment settings. For example; spring.cloud.stream.binders.rabbit1.environment. . . , spring.cloud.stream.binders.rabbit2.environment. . .

    spring.cloud.stream.binding-retry-interval

    30

    Retry interval (in seconds) used to schedule binding attempts. Default: 30 sec.

    spring.cloud.stream.bindings

     

    Additional binding properties (see {@link BinderProperties}) per binding name (e.g., 'input`). For example; This sets the content-type for the 'input' binding of a Sink application: 'spring.cloud.stream.bindings.input.contentType=text/plain'

    spring.cloud.stream.consul.binder.event-timeout

    5

     

    spring.cloud.stream.default-binder

     

    The name of the binder to use by all bindings in the event multiple binders available (e.g., 'rabbit').

    spring.cloud.stream.dynamic-destinations

    []

    A list of destinations that can be bound dynamically. If set, only listed destinations can be bound.

    spring.cloud.stream.function.definition

     

    Definition of functions to bind. If several functions need to be composed into one, use pipes (e.g., 'fooFunc\|barFunc')

    spring.cloud.stream.instance-count

    1

    The number of deployed instances of an application. Default: 1. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-count" where 'foo' is the name of the binding.

    spring.cloud.stream.instance-index

    0

    The instance id of the application: a number from 0 to instanceCount-1. Used for partitioning and with Kafka. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-index" where 'foo' is the name of the binding.

    spring.cloud.stream.integration.message-handler-not-propagated-headers

     

    Message header names that will NOT be copied from the inbound message.

    spring.cloud.stream.kafka.binder.auto-add-partitions

    false

     

    spring.cloud.stream.kafka.binder.auto-create-topics

    true

     

    spring.cloud.stream.kafka.binder.brokers

    [localhost]

     

    spring.cloud.stream.kafka.binder.configuration

     

    Arbitrary kafka properties that apply to both producers and consumers.

    spring.cloud.stream.kafka.binder.consumer-properties

     

    Arbitrary kafka consumer properties.

    spring.cloud.stream.kafka.binder.fetch-size

    0

     

    spring.cloud.stream.kafka.binder.header-mapper-bean-name

     

    The bean name of a custom header mapper to use instead of a {@link org.springframework.kafka.support.DefaultKafkaHeaderMapper}.

    spring.cloud.stream.kafka.binder.headers

    []

     

    spring.cloud.stream.kafka.binder.health-timeout

    60

    Time to wait to get partition information in seconds; default 60.

    spring.cloud.stream.kafka.binder.jaas

      

    spring.cloud.stream.kafka.binder.max-wait

    100

     

    spring.cloud.stream.kafka.binder.min-partition-count

    1

     

    spring.cloud.stream.kafka.binder.offset-update-count

    0

     

    spring.cloud.stream.kafka.binder.offset-update-shutdown-timeout

    2000

     

    spring.cloud.stream.kafka.binder.offset-update-time-window

    10000

     

    spring.cloud.stream.kafka.binder.producer-properties

     

    Arbitrary kafka producer properties.

    spring.cloud.stream.kafka.binder.queue-size

    8192

     

    spring.cloud.stream.kafka.binder.replication-factor

    1

     

    spring.cloud.stream.kafka.binder.required-acks

    1

     

    spring.cloud.stream.kafka.binder.socket-buffer-size

    2097152

     

    spring.cloud.stream.kafka.binder.transaction.producer.admin

      

    spring.cloud.stream.kafka.binder.transaction.producer.batch-timeout

      

    spring.cloud.stream.kafka.binder.transaction.producer.buffer-size

      

    spring.cloud.stream.kafka.binder.transaction.producer.compression-type

      

    spring.cloud.stream.kafka.binder.transaction.producer.configuration

      

    spring.cloud.stream.kafka.binder.transaction.producer.error-channel-enabled

      

    spring.cloud.stream.kafka.binder.transaction.producer.header-mode

      

    spring.cloud.stream.kafka.binder.transaction.producer.header-patterns

      

    spring.cloud.stream.kafka.binder.transaction.producer.message-key-expression

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-count

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-key-expression

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-key-extractor-name

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-selector-expression

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-selector-name

      

    spring.cloud.stream.kafka.binder.transaction.producer.required-groups

      

    spring.cloud.stream.kafka.binder.transaction.producer.sync

      

    spring.cloud.stream.kafka.binder.transaction.producer.topic

      

    spring.cloud.stream.kafka.binder.transaction.producer.use-native-encoding

      

    spring.cloud.stream.kafka.binder.transaction.transaction-id-prefix

      

    spring.cloud.stream.kafka.binder.zk-connection-timeout

    10000

    ZK Connection timeout in milliseconds.

    spring.cloud.stream.kafka.binder.zk-nodes

    [localhost]

     

    spring.cloud.stream.kafka.binder.zk-session-timeout

    10000

    ZK session timeout in milliseconds.

    spring.cloud.stream.kafka.bindings

      

    spring.cloud.stream.kafka.streams.binder.application-id

      

    spring.cloud.stream.kafka.streams.binder.auto-add-partitions

      

    spring.cloud.stream.kafka.streams.binder.auto-create-topics

      

    spring.cloud.stream.kafka.streams.binder.brokers

      

    spring.cloud.stream.kafka.streams.binder.configuration

      

    spring.cloud.stream.kafka.streams.binder.consumer-properties

      

    spring.cloud.stream.kafka.streams.binder.fetch-size

      

    spring.cloud.stream.kafka.streams.binder.header-mapper-bean-name

      

    spring.cloud.stream.kafka.streams.binder.headers

      

    spring.cloud.stream.kafka.streams.binder.health-timeout

      

    spring.cloud.stream.kafka.streams.binder.jaas

      

    spring.cloud.stream.kafka.streams.binder.max-wait

      

    spring.cloud.stream.kafka.streams.binder.min-partition-count

      

    spring.cloud.stream.kafka.streams.binder.offset-update-count

      

    spring.cloud.stream.kafka.streams.binder.offset-update-shutdown-timeout

      

    spring.cloud.stream.kafka.streams.binder.offset-update-time-window

      

    spring.cloud.stream.kafka.streams.binder.producer-properties

      

    spring.cloud.stream.kafka.streams.binder.queue-size

      

    spring.cloud.stream.kafka.streams.binder.replication-factor

      

    spring.cloud.stream.kafka.streams.binder.required-acks

      

    spring.cloud.stream.kafka.streams.binder.serde-error

     

    {@link org.apache.kafka.streams.errors.DeserializationExceptionHandler} to use when there is a Serde error. {@link KafkaStreamsBinderConfigurationProperties.SerdeError} values are used to provide the exception handler on consumer binding.

    spring.cloud.stream.kafka.streams.binder.socket-buffer-size

      

    spring.cloud.stream.kafka.streams.binder.zk-connection-timeout

      

    spring.cloud.stream.kafka.streams.binder.zk-nodes

      

    spring.cloud.stream.kafka.streams.binder.zk-session-timeout

      

    spring.cloud.stream.kafka.streams.bindings

      

    spring.cloud.stream.kafka.streams.time-window.advance-by

    0

     

    spring.cloud.stream.kafka.streams.time-window.length

    0

     

    spring.cloud.stream.metrics.export-properties

     

    List of properties that are going to be appended to each message. This gets populate by onApplicationEvent, once the context refreshes to avoid overhead of doing per message basis.

    spring.cloud.stream.metrics.key

     

    The name of the metric being emitted. Should be an unique value per application. Defaults to: ${spring.application.name:${vcap.application.name:${spring.config.name:application}}}.

    spring.cloud.stream.metrics.meter-filter

     

    Pattern to control the 'meters' one wants to capture. By default all 'meters' will be captured. For example, 'spring.integration.*' will only capture metric information for meters whose name starts with 'spring.integration'.

    spring.cloud.stream.metrics.properties

     

    Application properties that should be added to the metrics payload For example: spring.application**.

    spring.cloud.stream.metrics.schedule-interval

    60s

    Interval expressed as Duration for scheduling metrics snapshots publishing. Defaults to 60 seconds

    spring.cloud.stream.override-cloud-connectors

    false

    This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). The typical usage of this property is to be nested in a customized environment when connecting to multiple systems.

    spring.cloud.stream.rabbit.binder.admin-addresses

    []

    Urls for management plugins; only needed for queue affinity.

    spring.cloud.stream.rabbit.binder.admin-adresses

      

    spring.cloud.stream.rabbit.binder.compression-level

    0

    Compression level for compressed bindings; see 'java.util.zip.Deflator'.

    spring.cloud.stream.rabbit.binder.connection-name-prefix

     

    Prefix for connection names from this binder.

    spring.cloud.stream.rabbit.binder.nodes

    []

    Cluster member node names; only needed for queue affinity.

    spring.cloud.stream.rabbit.bindings

      

    spring.cloud.stream.schema-registry-client.cached

    false

     

    spring.cloud.stream.schema-registry-client.endpoint

      

    spring.cloud.stream.schema.avro.dynamic-schema-generation-enabled

    false

     

    spring.cloud.stream.schema.avro.prefix

    vnd

     

    spring.cloud.stream.schema.avro.reader-schema

      

    spring.cloud.stream.schema.avro.schema-imports

     

    A list of files or directories that should be loaded first thus making them importable by subsequent schemas. Note that imported files should not reference each other. @parameter

    spring.cloud.stream.schema.avro.schema-locations

     

    The source directory of Apache Avro schema. This schema is used by this converter. If this schema depends on other schemas consider defining those those dependent ones in the {@link #schemaImports} @parameter

    spring.cloud.stream.schema.server.allow-schema-deletion

    false

    Boolean flag to enable/disable schema deletion.

    spring.cloud.stream.schema.server.path

     

    Prefix for configuration resource paths (default is empty). Useful when embedding in another application when you don’t want to change the context path or servlet path.

    spring.cloud.task.batch.command-line-runner-order

    0

    The order for the {@code CommandLineRunner} used to run batch jobs when {@code spring.cloud.task.batch.fail-on-job-failure=true}. Defaults to 0 (same as the {@link org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner}).

    spring.cloud.task.batch.events.chunk-order

     

    Properties for chunk listener order

    spring.cloud.task.batch.events.chunk.enabled

    true

    This property is used to determine if a task should listen for batch chunk events.

    spring.cloud.task.batch.events.enabled

    true

    This property is used to determine if a task should listen for batch events.

    spring.cloud.task.batch.events.item-process-order

     

    Properties for itemProcess listener order

    spring.cloud.task.batch.events.item-process.enabled

    true

    This property is used to determine if a task should listen for batch item processed events.

    spring.cloud.task.batch.events.item-read-order

     

    Properties for itemRead listener order

    spring.cloud.task.batch.events.item-read.enabled

    true

    This property is used to determine if a task should listen for batch item read events.

    spring.cloud.task.batch.events.item-write-order

     

    Properties for itemWrite listener order

    spring.cloud.task.batch.events.item-write.enabled

    true

    This property is used to determine if a task should listen for batch item write events.

    spring.cloud.task.batch.events.job-execution-order

     

    Properties for jobExecution listener order

    spring.cloud.task.batch.events.job-execution.enabled

    true

    This property is used to determine if a task should listen for batch job execution events.

    spring.cloud.task.batch.events.skip-order

     

    Properties for skip listener order

    spring.cloud.task.batch.events.skip.enabled

    true

    This property is used to determine if a task should listen for batch skip events.

    spring.cloud.task.batch.events.step-execution-order

     

    Properties for stepExecution listener order

    spring.cloud.task.batch.events.step-execution.enabled

    true

    This property is used to determine if a task should listen for batch step execution events.

    spring.cloud.task.batch.fail-on-job-failure

    false

    This property is used to determine if a task app should return with a non zero exit code if a batch job fails.

    spring.cloud.task.batch.fail-on-job-failure-poll-interval

    5000

    Fixed delay in milliseconds that Spring Cloud Task will wait when checking if {@link org.springframework.batch.core.JobExecution}s have completed, when spring.cloud.task.batch.failOnJobFailure is set to true. Defaults to 5000.

    spring.cloud.task.batch.job-names

     

    Comma-separated list of job names to execute on startup (for instance, job1,job2). By default, all Jobs found in the context are executed.

    spring.cloud.task.batch.listener.enabled

    true

    This property is used to determine if a task will be linked to the batch jobs that are run.

    spring.cloud.task.closecontext-enabled

    false

    When set to true the context is closed at the end of the task. Else the context remains open.

    spring.cloud.task.events.enabled

    true

    This property is used to determine if a task app should emit task events.

    spring.cloud.task.executionid

     

    An id that will be used by the task when updating the task execution.

    spring.cloud.task.external-execution-id

     

    An id that can be associated with a task.

    spring.cloud.task.parent-execution-id

     

    The id of the parent task execution id that launched this task execution. Defaults to null if task execution had no parent.

    spring.cloud.task.single-instance-enabled

    false

    This property is used to determine if a task will execute if another task with the same app name is running.

    spring.cloud.task.single-instance-lock-check-interval

    500

    Declares the time (in millis) that a task execution will wait between checks. Default time is: 500 millis.

    spring.cloud.task.single-instance-lock-ttl

     

    Declares the maximum amount of time (in millis) that a task execution can hold a lock to prevent another task from executing with a specific task name when the single-instance-enabled is set to true. Default time is: Integer.MAX_VALUE.

    spring.cloud.task.table-prefix

    TASK_

    The prefix to append to the table names created by Spring Cloud Task.

    spring.cloud.util.enabled

    true

    Enables creation of Spring Cloud utility beans.

    spring.cloud.vault.app-id.app-id-path

    app-id

    Mount path of the AppId authentication backend.

    spring.cloud.vault.app-id.network-interface

     

    Network interface hint for the "MAC_ADDRESS" UserId mechanism.

    spring.cloud.vault.app-id.user-id

    MAC_ADDRESS

    UserId mechanism. Can be either "MAC_ADDRESS", "IP_ADDRESS", a string or a class name.

    spring.cloud.vault.app-role.app-role-path

    approle

    Mount path of the AppRole authentication backend.

    spring.cloud.vault.app-role.role

     

    Name of the role, optional, used for pull-mode.

    spring.cloud.vault.app-role.role-id

     

    The RoleId.

    spring.cloud.vault.app-role.secret-id

     

    The SecretId.

    spring.cloud.vault.application-name

    application

    Application name for AppId authentication.

    spring.cloud.vault.authentication

      

    spring.cloud.vault.aws-ec2.aws-ec2-path

    aws-ec2

    Mount path of the AWS-EC2 authentication backend.

    spring.cloud.vault.aws-ec2.identity-document

    http://169.254.169.254/latest/dynamic/instance-identity/pkcs7

    URL of the AWS-EC2 PKCS7 identity document.

    spring.cloud.vault.aws-ec2.nonce

     

    Nonce used for AWS-EC2 authentication. An empty nonce defaults to nonce generation.

    spring.cloud.vault.aws-ec2.role

     

    Name of the role, optional.

    spring.cloud.vault.aws-iam.aws-path

    aws

    Mount path of the AWS authentication backend.

    spring.cloud.vault.aws-iam.role

     

    Name of the role, optional. Defaults to the friendly IAM name if not set.

    spring.cloud.vault.aws-iam.server-name

     

    Name of the server used to set {@code X-Vault-AWS-IAM-Server-ID} header in the headers of login requests.

    spring.cloud.vault.aws.access-key-property

    cloud.aws.credentials.accessKey

    Target property for the obtained access key.

    spring.cloud.vault.aws.backend

    aws

    aws backend path.

    spring.cloud.vault.aws.enabled

    false

    Enable aws backend usage.

    spring.cloud.vault.aws.role

     

    Role name for credentials.

    spring.cloud.vault.aws.secret-key-property

    cloud.aws.credentials.secretKey

    Target property for the obtained secret key.

    spring.cloud.vault.azure-msi.azure-path

    azure

    Mount path of the Azure MSI authentication backend.

    spring.cloud.vault.azure-msi.role

     

    Name of the role.

    spring.cloud.vault.cassandra.backend

    cassandra

    Cassandra backend path.

    spring.cloud.vault.cassandra.enabled

    false

    Enable cassandra backend usage.

    spring.cloud.vault.cassandra.password-property

    spring.data.cassandra.password

    Target property for the obtained password.

    spring.cloud.vault.cassandra.role

     

    Role name for credentials.

    spring.cloud.vault.cassandra.username-property

    spring.data.cassandra.username

    Target property for the obtained username.

    spring.cloud.vault.config.lifecycle.enabled

    true

    Enable lifecycle management.

    spring.cloud.vault.config.order

    0

    Used to set a {@link org.springframework.core.env.PropertySource} priority. This is useful to use Vault as an override on other property sources. @see org.springframework.core.PriorityOrdered

    spring.cloud.vault.connection-timeout

    5000

    Connection timeout.

    spring.cloud.vault.consul.backend

    consul

    Consul backend path.

    spring.cloud.vault.consul.enabled

    false

    Enable consul backend usage.

    spring.cloud.vault.consul.role

     

    Role name for credentials.

    spring.cloud.vault.consul.token-property

    spring.cloud.consul.token

    Target property for the obtained token.

    spring.cloud.vault.database.backend

    database

    Database backend path.

    spring.cloud.vault.database.enabled

    false

    Enable database backend usage.

    spring.cloud.vault.database.password-property

    spring.datasource.password

    Target property for the obtained password.

    spring.cloud.vault.database.role

     

    Role name for credentials.

    spring.cloud.vault.database.username-property

    spring.datasource.username

    Target property for the obtained username.

    spring.cloud.vault.discovery.enabled

    false

    Flag to indicate that Vault server discovery is enabled (vault server URL will be looked up via discovery).

    spring.cloud.vault.discovery.service-id

    vault

    Service id to locate Vault.

    spring.cloud.vault.enabled

    true

    Enable Vault config server.

    spring.cloud.vault.fail-fast

    false

    Fail fast if data cannot be obtained from Vault.

    spring.cloud.vault.gcp-gce.gcp-path

    gcp

    Mount path of the Kubernetes authentication backend.

    spring.cloud.vault.gcp-gce.role

     

    Name of the role against which the login is being attempted.

    spring.cloud.vault.gcp-gce.service-account

     

    Optional service account id. Using the default id if left unconfigured.

    spring.cloud.vault.gcp-iam.credentials.encoded-key

     

    The base64 encoded contents of an OAuth2 account private key in JSON format.

    spring.cloud.vault.gcp-iam.credentials.location

     

    Location of the OAuth2 credentials private key. <p> Since this is a Resource, the private key can be in a multitude of locations, such as a local file system, classpath, URL, etc.

    spring.cloud.vault.gcp-iam.gcp-path

    gcp

    Mount path of the Kubernetes authentication backend.

    spring.cloud.vault.gcp-iam.jwt-validity

    15m

    Validity of the JWT token.

    spring.cloud.vault.gcp-iam.project-id

     

    Overrides the GCP project Id.

    spring.cloud.vault.gcp-iam.role

     

    Name of the role against which the login is being attempted.

    spring.cloud.vault.gcp-iam.service-account-id

     

    Overrides the GCP service account Id.

    spring.cloud.vault.generic.application-name

    application

    Application name to be used for the context.

    spring.cloud.vault.generic.backend

    secret

    Name of the default backend.

    spring.cloud.vault.generic.default-context

    application

    Name of the default context.

    spring.cloud.vault.generic.enabled

    true

    Enable the generic backend.

    spring.cloud.vault.generic.profile-separator

    /

    Profile-separator to combine application name and profile.

    spring.cloud.vault.host

    localhost

    Vault server host.

    spring.cloud.vault.kubernetes.kubernetes-path

    kubernetes

    Mount path of the Kubernetes authentication backend.

    spring.cloud.vault.kubernetes.role

     

    Name of the role against which the login is being attempted.

    spring.cloud.vault.kubernetes.service-account-token-file

    /var/run/secrets/kubernetes.io/serviceaccount/token

    Path to the service account token file.

    spring.cloud.vault.kv.application-name

    application

    Application name to be used for the context.

    spring.cloud.vault.kv.backend

    secret

    Name of the default backend.

    spring.cloud.vault.kv.backend-version

    2

    Key-Value backend version. Currently supported versions are: <ul> <li>Version 1 (unversioned key-value backend).</li> <li>Version 2 (versioned key-value backend).</li> </ul>

    spring.cloud.vault.kv.default-context

    application

    Name of the default context.

    spring.cloud.vault.kv.enabled

    false

    Enable the kev-value backend.

    spring.cloud.vault.kv.profile-separator

    /

    Profile-separator to combine application name and profile.

    spring.cloud.vault.mongodb.backend

    mongodb

    Cassandra backend path.

    spring.cloud.vault.mongodb.enabled

    false

    Enable mongodb backend usage.

    spring.cloud.vault.mongodb.password-property

    spring.data.mongodb.password

    Target property for the obtained password.

    spring.cloud.vault.mongodb.role

     

    Role name for credentials.

    spring.cloud.vault.mongodb.username-property

    spring.data.mongodb.username

    Target property for the obtained username.

    spring.cloud.vault.mysql.backend

    mysql

    mysql backend path.

    spring.cloud.vault.mysql.enabled

    false

    Enable mysql backend usage.

    spring.cloud.vault.mysql.password-property

    spring.datasource.password

    Target property for the obtained username.

    spring.cloud.vault.mysql.role

     

    Role name for credentials.

    spring.cloud.vault.mysql.username-property

    spring.datasource.username

    Target property for the obtained username.

    spring.cloud.vault.port

    8200

    Vault server port.

    spring.cloud.vault.postgresql.backend

    postgresql

    postgresql backend path.

    spring.cloud.vault.postgresql.enabled

    false

    Enable postgresql backend usage.

    spring.cloud.vault.postgresql.password-property

    spring.datasource.password

    Target property for the obtained username.

    spring.cloud.vault.postgresql.role

     

    Role name for credentials.

    spring.cloud.vault.postgresql.username-property

    spring.datasource.username

    Target property for the obtained username.

    spring.cloud.vault.rabbitmq.backend

    rabbitmq

    rabbitmq backend path.

    spring.cloud.vault.rabbitmq.enabled

    false

    Enable rabbitmq backend usage.

    spring.cloud.vault.rabbitmq.password-property

    spring.rabbitmq.password

    Target property for the obtained password.

    spring.cloud.vault.rabbitmq.role

     

    Role name for credentials.

    spring.cloud.vault.rabbitmq.username-property

    spring.rabbitmq.username

    Target property for the obtained username.

    spring.cloud.vault.read-timeout

    15000

    Read timeout.

    spring.cloud.vault.scheme

    https

    Protocol scheme. Can be either "http" or "https".

    spring.cloud.vault.ssl.cert-auth-path

    cert

    Mount path of the TLS cert authentication backend.

    spring.cloud.vault.ssl.key-store

     

    Trust store that holds certificates and private keys.

    spring.cloud.vault.ssl.key-store-password

     

    Password used to access the key store.

    spring.cloud.vault.ssl.trust-store

     

    Trust store that holds SSL certificates.

    spring.cloud.vault.ssl.trust-store-password

     

    Password used to access the trust store.

    spring.cloud.vault.token

     

    Static vault token. Required if {@link #authentication} is {@code TOKEN}.

    spring.cloud.vault.uri

     

    Vault URI. Can be set with scheme, host and port.

    spring.cloud.zookeeper.base-sleep-time-ms

    50

    Initial amount of time to wait between retries.

    spring.cloud.zookeeper.block-until-connected-unit

     

    The unit of time related to blocking on connection to Zookeeper.

    spring.cloud.zookeeper.block-until-connected-wait

    10

    Wait time to block on connection to Zookeeper.

    spring.cloud.zookeeper.connect-string

    localhost:2181

    Connection string to the Zookeeper cluster.

    spring.cloud.zookeeper.default-health-endpoint

     

    Default health endpoint that will be checked to verify that a dependency is alive.

    spring.cloud.zookeeper.dependencies

     

    Mapping of alias to ZookeeperDependency. From Ribbon perspective the alias is actually serviceID since Ribbon can’t accept nested structures in serviceID.

    spring.cloud.zookeeper.dependency-configurations

      

    spring.cloud.zookeeper.dependency-names

      

    spring.cloud.zookeeper.discovery.enabled

    true

     

    spring.cloud.zookeeper.discovery.initial-status

     

    The initial status of this instance (defaults to {@link StatusConstants#STATUS_UP}).

    spring.cloud.zookeeper.discovery.instance-host

     

    Predefined host with which a service can register itself in Zookeeper. Corresponds to the {code address} from the URI spec.

    spring.cloud.zookeeper.discovery.instance-id

     

    Id used to register with zookeeper. Defaults to a random UUID.

    spring.cloud.zookeeper.discovery.instance-port

     

    Port to register the service under (defaults to listening port).

    spring.cloud.zookeeper.discovery.instance-ssl-port

     

    Ssl port of the registered service.

    spring.cloud.zookeeper.discovery.metadata

     

    Gets the metadata name/value pairs associated with this instance. This information is sent to zookeeper and can be used by other instances.

    spring.cloud.zookeeper.discovery.order

    0

    Order of the discovery client used by CompositeDiscoveryClient for sorting available clients.

    spring.cloud.zookeeper.discovery.register

    true

    Register as a service in zookeeper.

    spring.cloud.zookeeper.discovery.root

    /services

    Root Zookeeper folder in which all instances are registered.

    spring.cloud.zookeeper.discovery.uri-spec

    {scheme}://{address}:{port}

    The URI specification to resolve during service registration in Zookeeper.

    spring.cloud.zookeeper.enabled

    true

    Is Zookeeper enabled.

    spring.cloud.zookeeper.max-retries

    10

    Max number of times to retry.

    spring.cloud.zookeeper.max-sleep-ms

    500

    Max time in ms to sleep on each retry.

    spring.cloud.zookeeper.prefix

     

    Common prefix that will be applied to all Zookeeper dependencies' paths.

    spring.integration.poller.fixed-delay

    1000

    Fixed delay for default poller.

    spring.integration.poller.max-messages-per-poll

    1

    Maximum messages per poll for the default poller.

    spring.sleuth.annotation.enabled

    true

     

    spring.sleuth.async.configurer.enabled

    true

    Enable default AsyncConfigurer.

    spring.sleuth.async.enabled

    true

    Enable instrumenting async related components so that the tracing information is passed between threads.

    spring.sleuth.async.ignored-beans

     

    List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation.

    spring.sleuth.baggage-keys

     

    List of baggage key names that should be propagated out of process. These keys will be prefixed with baggage before the actual key. This property is set in order to be backward compatible with previous Sleuth versions. @see brave.propagation.ExtraFieldPropagation.FactoryBuilder#addPrefixedFields(String, java.util.Collection)

    spring.sleuth.enabled

    true

     

    spring.sleuth.feign.enabled

    true

    Enable span information propagation when using Feign.

    spring.sleuth.feign.processor.enabled

    true

    Enable post processor that wraps Feign Context in its tracing representations.

    spring.sleuth.grpc.enabled

    true

    Enable span information propagation when using GRPC.

    spring.sleuth.http.enabled

    true

     

    spring.sleuth.http.legacy.enabled

    false

    Enables the legacy Sleuth setup.

    spring.sleuth.hystrix.strategy.enabled

    true

    Enable custom HystrixConcurrencyStrategy that wraps all Callable instances into their Sleuth representative - the TraceCallable.

    spring.sleuth.integration.enabled

    true

    Enable Spring Integration sleuth instrumentation.

    spring.sleuth.integration.patterns

    [!hystrixStreamOutput*, *]

    An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream channel name.

    spring.sleuth.integration.websockets.enabled

    true

    Enable tracing for WebSockets.

    spring.sleuth.keys.http.headers

     

    Additional headers that should be added as tags if they exist. If the header value is multi-valued, the tag value will be a comma-separated, single-quoted list.

    spring.sleuth.keys.http.prefix

    http.

    Prefix for header names if they are added as tags.

    spring.sleuth.log.slf4j.enabled

    true

    Enable a {@link Slf4jScopeDecorator} that prints tracing information in the logs.

    spring.sleuth.log.slf4j.whitelisted-mdc-keys

     

    A list of keys to be put from baggage to MDC.

    spring.sleuth.messaging.enabled

    false

    Should messaging be turned on.

    spring.sleuth.messaging.jms.enabled

    false

     

    spring.sleuth.messaging.jms.remote-service-name

    jms

     

    spring.sleuth.messaging.kafka.enabled

    false

     

    spring.sleuth.messaging.kafka.remote-service-name

    kafka

     

    spring.sleuth.messaging.rabbit.enabled

    false

     

    spring.sleuth.messaging.rabbit.remote-service-name

    rabbitmq

     

    spring.sleuth.opentracing.enabled

    true

     

    spring.sleuth.propagation-keys

     

    List of fields that are referenced the same in-process as it is on the wire. For example, the name "x-vcap-request-id" would be set as-is including the prefix. <p> Note: {@code fieldName} will be implicitly lower-cased. @see brave.propagation.ExtraFieldPropagation.FactoryBuilder#addField(String)

    spring.sleuth.propagation.tag.enabled

    true

    Enables a {@link TagPropagationFinishedSpanHandler} that adds extra propagated fields to span tags.

    spring.sleuth.propagation.tag.whitelisted-keys

     

    A list of keys to be put from extra propagation fields to span tags.

    spring.sleuth.reactor.decorate-on-each

    true

    When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries.

    spring.sleuth.reactor.enabled

    true

    When true enables instrumentation for reactor.

    spring.sleuth.rxjava.schedulers.hook.enabled

    true

    Enable support for RxJava via RxJavaSchedulersHook.

    spring.sleuth.rxjava.schedulers.ignoredthreads

    [HystrixMetricPoller, ^RxComputation.*$]

    Thread names for which spans will not be sampled.

    spring.sleuth.sampler.probability

    0.1

    Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there’s no support for 0.1% of the traces).

    spring.sleuth.sampler.rate

     

    A rate per second can be a nice choice for low-traffic endpoints as it allows you surge protection. For example, you may never expect the endpoint to get more than 50 requests per second. If there was a sudden surge of traffic, to 5000 requests per second, you would still end up with 50 traces per second. Conversely, if you had a percentage, like 10%, the same surge would end up with 500 traces per second, possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler (named Reservoir) for this purpose. Brave has taken the same approach via the {@link brave.sampler.RateLimitingSampler}.

    spring.sleuth.scheduled.enabled

    true

    Enable tracing for {@link org.springframework.scheduling.annotation.Scheduled}.

    spring.sleuth.scheduled.skip-pattern

    org.springframework.cloud.netflix.hystrix.stream.HystrixStreamTask

    Pattern for the fully qualified name of a class that should be skipped.

    spring.sleuth.supports-join

    true

    True means the tracing system supports sharing a span ID between a client and server.

    spring.sleuth.trace-id128

    false

    When true, generate 128-bit trace IDs instead of 64-bit ones.

    spring.sleuth.web.additional-skip-pattern

     

    Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}.

    spring.sleuth.web.client.enabled

    true

    Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}.

    spring.sleuth.web.client.skip-pattern

     

    Pattern for URLs that should be skipped in client side tracing.

    spring.sleuth.web.enabled

    true

    When true enables instrumentation for web applications.

    spring.sleuth.web.exception-logging-filter-enabled

    true

    Flag to toggle the presence of a filter that logs thrown exceptions.

    spring.sleuth.web.exception-throwing-filter-enabled

    true

    Flag to toggle the presence of a filter that logs thrown exceptions. @deprecated use {@link #exceptionLoggingFilterEnabled}

    spring.sleuth.web.filter-order

     

    Order in which the tracing filters should be registered. Defaults to {@link TraceHttpAutoConfiguration#TRACING_FILTER_ORDER}.

    spring.sleuth.web.ignore-auto-configured-skip-patterns

    false

    If set to true, auto-configured skip patterns will be ignored. @see TraceWebAutoConfiguration

    spring.sleuth.web.skip-pattern

    /api-docs.|/swagger.|.\.png|.\.css|.\.js|.\.html|/favicon.ico|/hystrix.stream

    Pattern for URLs that should be skipped in tracing.

    spring.sleuth.zuul.enabled

    true

    Enable span information propagation when using Zuul.

    spring.zipkin.base-url

    http://localhost:9411/

    URL of the zipkin query server instance. You can also provide the service id of the Zipkin server if Zipkin’s registered in service discovery (e.g. http://zipkinserver/).

    spring.zipkin.compression.enabled

    false

     

    spring.zipkin.discovery-client-enabled

     

    If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL always.

    spring.zipkin.enabled

    true

    Enables sending spans to Zipkin.

    spring.zipkin.encoder

     

    Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server is not recent.

    spring.zipkin.locator.discovery.enabled

    false

    Enabling of locating the host name via service discovery.

    spring.zipkin.message-timeout

    1

    Timeout in seconds before pending spans will be sent in batches to Zipkin.

    spring.zipkin.sender.type

     

    Means of sending spans to Zipkin.

    spring.zipkin.service.name

     

    The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin.

    stubrunner.amqp.enabled

    false

    Whether to enable support for Stub Runner and AMQP.

    stubrunner.amqp.mockCOnnection

    true

    Whether to enable support for Stub Runner and AMQP mocked connection factory.

    stubrunner.classifier

    stubs

    The classifier to use by default in ivy co-ordinates for a stub.

    stubrunner.cloud.consul.enabled

    true

    Whether to enable stubs registration in Consul.

    stubrunner.cloud.delegate.enabled

    true

    Whether to enable DiscoveryClient’s Stub Runner implementation.

    stubrunner.cloud.enabled

    true

    Whether to enable Spring Cloud support for Stub Runner.

    stubrunner.cloud.eureka.enabled

    true

    Whether to enable stubs registration in Eureka.

    stubrunner.cloud.ribbon.enabled

    true

    Whether to enable Stub Runner’s Ribbon integration.

    stubrunner.cloud.stubbed.discovery.enabled

    true

    Whether Service Discovery should be stubbed for Stub Runner. If set to false, stubs will get registered in real service discovery.

    stubrunner.cloud.zookeeper.enabled

    true

    Whether to enable stubs registration in Zookeeper.

    stubrunner.consumer-name

     

    You can override the default {@code spring.application.name} of this field by setting a value to this parameter.

    stubrunner.delete-stubs-after-test

    true

    If set to {@code false} will NOT delete stubs from a temporary folder after running tests.

    stubrunner.http-server-stub-configurer

     

    Configuration for an HTTP server stub.

    stubrunner.ids

    []

    The ids of the stubs to run in "ivy" notation ([groupId]:artifactId:[version]:[classifier][:port]). {@code groupId}, {@code classifier}, {@code version} and {@code port} can be optional.

    stubrunner.ids-to-service-ids

     

    Mapping of Ivy notation based ids to serviceIds inside your application. Example "a:b" → "myService" "artifactId" → "myOtherService"

    stubrunner.integration.enabled

    true

    Whether to enable Stub Runner integration with Spring Integration.

    stubrunner.mappings-output-folder

     

    Dumps the mappings of each HTTP server to the selected folder.

    stubrunner.max-port

    15000

    Max value of a port for the automatically started WireMock server.

    stubrunner.min-port

    10000

    Min value of a port for the automatically started WireMock server.

    stubrunner.password

     

    Repository password.

    stubrunner.properties

     

    Map of properties that can be passed to custom {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.

    stubrunner.proxy-host

     

    Repository proxy host.

    stubrunner.proxy-port

     

    Repository proxy port.

    stubrunner.stream.enabled

    true

    Whether to enable Stub Runner integration with Spring Cloud Stream.

    stubrunner.stubs-mode

     

    Pick where the stubs should come from.

    stubrunner.stubs-per-consumer

    false

    Should only stubs for this particular consumer get registered in HTTP server stub.

    stubrunner.username

     

    Repository username.

    wiremock.rest-template-ssl-enabled

    false

     

    wiremock.server.files

    []

     

    wiremock.server.https-port

    -1

     

    wiremock.server.https-port-dynamic

    false

     

    wiremock.server.port

    8080

     

    wiremock.server.port-dynamic

    false

     

    wiremock.server.stubs

    []

     
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__binder_implementations.html b/Greenwich.SR5/multi/multi__binder_implementations.html new file mode 100644 index 00000000..d21807fe --- /dev/null +++ b/Greenwich.SR5/multi/multi__binder_implementations.html @@ -0,0 +1,3 @@ + + + Part VI. Binder Implementations

    Part VI. Binder Implementations

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__broadcasting_your_own_events.html b/Greenwich.SR5/multi/multi__broadcasting_your_own_events.html new file mode 100644 index 00000000..a833b80b --- /dev/null +++ b/Greenwich.SR5/multi/multi__broadcasting_your_own_events.html @@ -0,0 +1,34 @@ + + + 49. Broadcasting Your Own Events

    49. Broadcasting Your Own Events

    The Bus can carry any event of type RemoteApplicationEvent. The default transport is +JSON, and the deserializer needs to know which types are going to be used ahead of time. +To register a new type, you must put it in a subpackage of +org.springframework.cloud.bus.event.

    To customise the event name, you can use @JsonTypeName on your custom class or rely on +the default strategy, which is to use the simple name of the class.

    [Note]Note

    Both the producer and the consumer need access to the class definition.

    49.1 Registering events in custom packages

    If you cannot or do not want to use a subpackage of org.springframework.cloud.bus.event +for your custom events, you must specify which packages to scan for events of type +RemoteApplicationEvent by using the @RemoteApplicationEventScan annotation. Packages +specified with @RemoteApplicationEventScan include subpackages.

    For example, consider the following custom event, called MyEvent:

    package com.acme;
    +
    +public class MyEvent extends RemoteApplicationEvent {
    +    ...
    +}

    You can register that event with the deserializer in the following way:

    package com.acme;
    +
    +@Configuration
    +@RemoteApplicationEventScan
    +public class BusConfiguration {
    +    ...
    +}

    Without specifying a value, the package of the class where @RemoteApplicationEventScan +is used is registered. In this example, com.acme is registered by using the package of +BusConfiguration.

    You can also explicitly specify the packages to scan by using the value, basePackages +or basePackageClasses properties on @RemoteApplicationEventScan, as shown in the +following example:

    package com.acme;
    +
    +@Configuration
    +//@RemoteApplicationEventScan({"com.acme", "foo.bar"})
    +//@RemoteApplicationEventScan(basePackages = {"com.acme", "foo.bar", "fizz.buzz"})
    +@RemoteApplicationEventScan(basePackageClasses = BusConfiguration.class)
    +public class BusConfiguration {
    +    ...
    +}

    All of the preceding examples of @RemoteApplicationEventScan are equivalent, in that the +com.acme package is registered by explicitly specifying the packages on +@RemoteApplicationEventScan.

    [Note]Note

    You can specify multiple base packages to scan.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__building.html b/Greenwich.SR5/multi/multi__building.html new file mode 100644 index 00000000..312c54f2 --- /dev/null +++ b/Greenwich.SR5/multi/multi__building.html @@ -0,0 +1,47 @@ + + + 149. Building

    149. Building

    149.1 Basic Compile and Test

    To build the source you will need to install JDK 1.7.

    Spring Cloud uses Maven for most build-related activities, and you +should be able to get off the ground quite quickly by cloning the +project you are interested in and typing

    $ ./mvnw install
    [Note]Note

    You can also install Maven (>=3.3.3) yourself and run the mvn command +in place of ./mvnw in the examples below. If you do that you also +might need to add -P spring if your local Maven settings do not +contain repository declarations for spring pre-release artifacts.

    [Note]Note

    Be aware that you might need to increase the amount of memory +available to Maven by setting a MAVEN_OPTS environment variable with +a value like -Xmx512m -XX:MaxPermSize=128m. We try to cover this in +the .mvn configuration, so if you find you have to do it to make a +build succeed, please raise a ticket to get the settings added to +source control.

    For hints on how to build the project look in .travis.yml if there +is one. There should be a "script" and maybe "install" command. Also +look at the "services" section to see if any services need to be +running locally (e.g. mongo or rabbit). Ignore the git-related bits +that you might find in "before_install" since they’re related to setting git +credentials and you already have those.

    The projects that require middleware generally include a +docker-compose.yml, so consider using +Docker Compose to run the middeware servers +in Docker containers. See the README in the +scripts demo +repository for specific instructions about the common cases of mongo, +rabbit and redis.

    [Note]Note

    If all else fails, build with the command from .travis.yml (usually +./mvnw install).

    149.2 Documentation

    The spring-cloud-build module has a "docs" profile, and if you switch +that on it will try to build asciidoc sources from +src/main/asciidoc. As part of that process it will look for a +README.adoc and process it by loading all the includes, but not +parsing or rendering it, just copying it to ${main.basedir} +(defaults to $../../../.., i.e. the root of the project). If there are +any changes in the README it will then show up after a Maven build as +a modified file in the correct place. Just commit it and push the change.

    149.3 Working with the code

    If you don’t have an IDE preference we would recommend that you use +Spring Tools Suite or +Eclipse when working with the code. We use the +m2eclipse eclipse plugin for maven support. Other IDEs and tools +should also work without issue as long as they use Maven 3.3.3 or better.

    149.3.1 Importing into eclipse with m2eclipse

    We recommend the m2eclipse eclipse plugin when working with +eclipse. If you don’t already have m2eclipse installed it is available from the "eclipse +marketplace".

    [Note]Note

    Older versions of m2e do not support Maven 3.3, so once the +projects are imported into Eclipse you will also need to tell +m2eclipse to use the right profile for the projects. If you +see many different errors related to the POMs in the projects, check +that you have an up to date installation. If you can’t upgrade m2e, +add the "spring" profile to your settings.xml. Alternatively you can +copy the repository settings from the "spring" profile of the parent +pom into your settings.xml.

    149.3.2 Importing into eclipse without m2eclipse

    If you prefer not to use m2eclipse you can generate eclipse project metadata using the +following command:

    $ ./mvnw eclipse:eclipse

    The generated eclipse projects can be imported by selecting import existing projects +from the file menu.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__building_a_simple_gateway_using_spring_mvc_or_webflux.html b/Greenwich.SR5/multi/multi__building_a_simple_gateway_using_spring_mvc_or_webflux.html new file mode 100644 index 00000000..caee1eab --- /dev/null +++ b/Greenwich.SR5/multi/multi__building_a_simple_gateway_using_spring_mvc_or_webflux.html @@ -0,0 +1,31 @@ + + + 125. Building a Simple Gateway Using Spring MVC or Webflux

    125. Building a Simple Gateway Using Spring MVC or Webflux

    [Warning]Warning

    The following describes an alternative style gateway. None of the prior documentation applies to what follows.

    Spring Cloud Gateway provides a utility object called ProxyExchange which you can use inside a regular Spring web handler as a method parameter. It supports basic downstream HTTP exchanges via methods that mirror the HTTP verbs. With MVC it also supports forwarding to a local handler via the forward() method. To use the ProxyExchange just include the right module in your classpath (either spring-cloud-gateway-mvc or spring-cloud-gateway-webflux).

    MVC example (proxying a request to "/test" downstream to a remote server):

    @RestController
    +@SpringBootApplication
    +public class GatewaySampleApplication {
    +
    +	@Value("${remote.home}")
    +	private URI home;
    +
    +	@GetMapping("/test")
    +	public ResponseEntity<?> proxy(ProxyExchange<byte[]> proxy) throws Exception {
    +		return proxy.uri(home.toString() + "/image/png").get();
    +	}
    +
    +}

    The same thing with Webflux:

    @RestController
    +@SpringBootApplication
    +public class GatewaySampleApplication {
    +
    +	@Value("${remote.home}")
    +	private URI home;
    +
    +	@GetMapping("/test")
    +	public Mono<ResponseEntity<?>> proxy(ProxyExchange<byte[]> proxy) throws Exception {
    +		return proxy.uri(home.toString() + "/image/png").get();
    +	}
    +
    +}

    There are convenience methods on the ProxyExchange to enable the handler method to discover and enhance the URI path of the incoming request. For example you might want to extract the trailing elements of a path to pass them downstream:

    @GetMapping("/proxy/path/**")
    +public ResponseEntity<?> proxyPath(ProxyExchange<byte[]> proxy) throws Exception {
    +  String path = proxy.path("/proxy/path/");
    +  return proxy.uri(home.toString() + "/foos/" + path).get();
    +}

    All the features of Spring MVC or Webflux are available to Gateway handler methods. So you can inject request headers and query parameters, for instance, and you can constrain the incoming requests with declarations in the mapping annotation. See the documentation for @RequestMapping in Spring MVC for more details of those features.

    Headers can be added to the downstream response using the header() methods on ProxyExchange.

    You can also manipulate response headers (and anything else you like in the response) by adding a mapper to the get() etc. method. The mapper is a Function that takes the incoming ResponseEntity and converts it to an outgoing one.

    First class support is provided for "sensitive" headers ("cookie" and "authorization" by default) which are not passed downstream, and for "proxy" headers (x-forwarded-*).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__building_and_running_a_function.html b/Greenwich.SR5/multi/multi__building_and_running_a_function.html new file mode 100644 index 00000000..16e82826 --- /dev/null +++ b/Greenwich.SR5/multi/multi__building_and_running_a_function.html @@ -0,0 +1,18 @@ + + + 128. Building and Running a Function

    128. Building and Running a Function

    The sample @SpringBootApplication above has a function that can be +decorated at runtime by Spring Cloud Function to be an HTTP endpoint, +or a Stream processor, for instance with RabbitMQ, Apache Kafka or +JMS.

    The @Beans can be Function, Consumer or Supplier (all from +java.util), and their parametric types can be String or POJO.

    Functions can also be of Flux<String> or Flux<Pojo> and Spring +Cloud Function takes care of converting the data to and from the +desired types, as long as it comes in as plain text or (in the case of +the POJO) JSON. There is also support for Message<Pojo> where the +message headers are copied from the incoming event, depending on the +adapter. The web adapter also supports conversion from form-encoded +data to a Map, and if you are using the function with Spring Cloud +Stream then all the conversion and coercion features for message +payloads will be applicable as well.

    Functions can be grouped together in a single application, or deployed +one-per-jar. It’s up to the developer to choose. An app with multiple +functions can be deployed multiple times in different "personalities", +exposing different functions over different physical transports.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__bus_endpoints.html b/Greenwich.SR5/multi/multi__bus_endpoints.html new file mode 100644 index 00000000..7464f48e --- /dev/null +++ b/Greenwich.SR5/multi/multi__bus_endpoints.html @@ -0,0 +1,13 @@ + + + 43. Bus Endpoints

    43. Bus Endpoints

    Spring Cloud Bus provides two endpoints, /actuator/bus-refresh and /actuator/bus-env +that correspond to individual actuator endpoints in Spring Cloud Commons, +/actuator/refresh and /actuator/env respectively.

    43.1 Bus Refresh Endpoint

    The /actuator/bus-refresh endpoint clears the RefreshScope cache and rebinds +@ConfigurationProperties. See the Refresh Scope documentation for +more information.

    To expose the /actuator/bus-refresh endpoint, you need to add following configuration to your +application:

    management.endpoints.web.exposure.include=bus-refresh

    43.2 Bus Env Endpoint

    The /actuator/bus-env endpoint updates each instances environment with the specified +key/value pair across multiple instances.

    To expose the /actuator/bus-env endpoint, you need to add following configuration to your +application:

    management.endpoints.web.exposure.include=bus-env

    The /actuator/bus-env endpoint accepts POST requests with the following shape:

    {
    +	"name": "key1",
    +	"value": "value1"
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__circuit_breaker_hystrix_clients.html b/Greenwich.SR5/multi/multi__circuit_breaker_hystrix_clients.html new file mode 100644 index 00000000..89a263ef --- /dev/null +++ b/Greenwich.SR5/multi/multi__circuit_breaker_hystrix_clients.html @@ -0,0 +1,61 @@ + + + 13. Circuit Breaker: Hystrix Clients

    13. Circuit Breaker: Hystrix Clients

    Netflix has created a library called Hystrix that implements the circuit breaker pattern. +In a microservice architecture, it is common to have multiple layers of service calls, as shown in the following example:

    Figure 13.1. Microservice Graph

    Hystrix

    A service failure in the lower level of services can cause cascading failure all the way up to the user. +When calls to a particular service exceed circuitBreaker.requestVolumeThreshold (default: 20 requests) and the failure percentage is greater than circuitBreaker.errorThresholdPercentage (default: >50%) in a rolling window defined by metrics.rollingStats.timeInMilliseconds (default: 10 seconds), the circuit opens and the call is not made. +In cases of error and an open circuit, a fallback can be provided by the developer.

    Figure 13.2. Hystrix fallback prevents cascading failures

    HystrixFallback

    Having an open circuit stops cascading failures and allows overwhelmed or failing services time to recover. +The fallback can be another Hystrix protected call, static data, or a sensible empty value. +Fallbacks may be chained so that the first fallback makes some other business call, which in turn falls back to static data.

    13.1 How to Include Hystrix

    To include Hystrix in your project, use the starter with a group ID of org.springframework.cloud +and a artifact ID of spring-cloud-starter-netflix-hystrix. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    The following example shows a minimal Eureka server with a Hystrix circuit breaker:

    @SpringBootApplication
    +@EnableCircuitBreaker
    +public class Application {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}
    +
    +@Component
    +public class StoreIntegration {
    +
    +    @HystrixCommand(fallbackMethod = "defaultStores")
    +    public Object getStores(Map<String, Object> parameters) {
    +        //do stuff that might fail
    +    }
    +
    +    public Object defaultStores(Map<String, Object> parameters) {
    +        return /* something useful */;
    +    }
    +}

    The @HystrixCommand is provided by a Netflix contrib library called javanica. +Spring Cloud automatically wraps Spring beans with that annotation in a proxy that is connected to the Hystrix circuit breaker. +The circuit breaker calculates when to open and close the circuit and what to do in case of a failure.

    To configure the @HystrixCommand you can use the commandProperties +attribute with a list of @HystrixProperty annotations. See +here +for more details. See the Hystrix wiki +for details on the properties available.

    13.2 Propagating the Security Context or Using Spring Scopes

    If you want some thread local context to propagate into a @HystrixCommand, the default declaration does not work, because it executes the command in a thread pool (in case of timeouts). +You can switch Hystrix to use the same thread as the caller through configuration or directly in the annotation, by asking it to use a different Isolation Strategy. +The following example demonstrates setting the thread in the annotation:

    @HystrixCommand(fallbackMethod = "stubMyService",
    +    commandProperties = {
    +      @HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
    +    }
    +)
    +...

    The same thing applies if you are using @SessionScope or @RequestScope. +If you encounter a runtime exception that says it cannot find the scoped context, you need to use the same thread.

    You also have the option to set the hystrix.shareSecurityContext property to true. +Doing so auto-configures a Hystrix concurrency strategy plugin hook to transfer the SecurityContext from your main thread to the one used by the Hystrix command. +Hystrix does not let multiple Hystrix concurrency strategy be registered so an extension mechanism is available by declaring your own HystrixConcurrencyStrategy as a Spring bean. +Spring Cloud looks for your implementation within the Spring context and wrap it inside its own plugin.

    13.3 Health Indicator

    The state of the connected circuit breakers are also exposed in the /health endpoint of the calling application, as shown in the following example:

    {
    +    "hystrix": {
    +        "openCircuitBreakers": [
    +            "StoreIntegration::getStoresByLocationLink"
    +        ],
    +        "status": "CIRCUIT_OPEN"
    +    },
    +    "status": "UP"
    +}

    13.4 Hystrix Metrics Stream

    To enable the Hystrix metrics stream, include a dependency on spring-boot-starter-actuator and set +management.endpoints.web.exposure.include: hystrix.stream. +Doing so exposes the /actuator/hystrix.stream as a management endpoint, as shown in the following example:

        <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-actuator</artifactId>
    +    </dependency>
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__circuit_breaker_hystrix_dashboard.html b/Greenwich.SR5/multi/multi__circuit_breaker_hystrix_dashboard.html new file mode 100644 index 00000000..5b14f179 --- /dev/null +++ b/Greenwich.SR5/multi/multi__circuit_breaker_hystrix_dashboard.html @@ -0,0 +1,4 @@ + + + 14. Circuit Breaker: Hystrix Dashboard

    14. Circuit Breaker: Hystrix Dashboard

    One of the main benefits of Hystrix is the set of metrics it gathers about each HystrixCommand. +The Hystrix Dashboard displays the health of each circuit breaker in an efficient manner.

    Figure 14.1. Hystrix Dashboard

    Hystrix

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__client_side_usage_2.html b/Greenwich.SR5/multi/multi__client_side_usage_2.html new file mode 100644 index 00000000..46b45fef --- /dev/null +++ b/Greenwich.SR5/multi/multi__client_side_usage_2.html @@ -0,0 +1,66 @@ + + + 101. Client Side Usage

    101. Client Side Usage

    To use these features in an application, just build it as a Spring +Boot application that depends on spring-cloud-vault-config (e.g. see +the test cases). Example Maven configuration:

    Example 101.1. pom.xml

    <parent>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-parent</artifactId>
    +    <version>2.0.0.RELEASE</version>
    +    <relativePath /> <!-- lookup parent from repository -->
    +</parent>
    +
    +<dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-starter-vault-config</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +    <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-test</artifactId>
    +        <scope>test</scope>
    +    </dependency>
    +</dependencies>
    +
    +<build>
    +    <plugins>
    +        <plugin>
    +            <groupId>org.springframework.boot</groupId>
    +            <artifactId>spring-boot-maven-plugin</artifactId>
    +        </plugin>
    +    </plugins>
    +</build>
    +
    +<!-- repositories also needed for snapshots and milestones -->

    Then you can create a standard Spring Boot application, like this simple HTTP server:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello World!";
    +    }
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(Application.class, args);
    +    }
    +}

    When it runs it will pick up the external configuration from the +default local Vault server on port 8200 if it is running. To modify +the startup behavior you can change the location of the Vault server +using bootstrap.properties (like application.properties but for +the bootstrap phase of an application context), e.g.

    Example 101.2. bootstrap.yml

    spring.cloud.vault:
    +    host: localhost
    +    port: 8200
    +    scheme: https
    +    uri: https://localhost:8200
    +    connection-timeout: 5000
    +    read-timeout: 15000
    +    config:
    +        order: -10

    • host sets the hostname of the Vault host. The host name will be used +for SSL certificate validation
    • port sets the Vault port
    • scheme setting the scheme to http will use plain HTTP. +Supported schemes are http and https.
    • uri configure the Vault endpoint with an URI. Takes precedence over host/port/scheme configuration
    • connection-timeout sets the connection timeout in milliseconds
    • read-timeout sets the read timeout in milliseconds
    • config.order sets the order for the property source

    Enabling further integrations requires additional dependencies and +configuration. Depending on how you have set up Vault you might need +additional configuration like +SSL and +authentication.

    If the application imports the spring-boot-starter-actuator project, the +status of the vault server will be available via the /health endpoint.

    The vault health indicator can be enabled or disabled through the property management.health.vault.enabled (default to true).

    101.1 Authentication

    Vault requires an authentication mechanism to authorize client requests.

    Spring Cloud Vault supports multiple authentication mechanisms to authenticate applications with Vault.

    For a quickstart, use the root token printed by the Vault initialization.

    Example 101.3. bootstrap.yml

    spring.cloud.vault:
    +    token: 19aefa97-cccc-bbbb-aaaa-225940e63d76

    [Warning]Warning

    Consider carefully your security requirements. Static token authentication is fine if you want quickly get started with Vault, but a static token is not protected any further. Any disclosure to unintended parties allows Vault use with the associated token roles.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__cloud_foundry.html b/Greenwich.SR5/multi/multi__cloud_foundry.html new file mode 100644 index 00000000..214b6b71 --- /dev/null +++ b/Greenwich.SR5/multi/multi__cloud_foundry.html @@ -0,0 +1,8 @@ + + + 168. Cloud Foundry

    168. Cloud Foundry

    Spring Cloud GCP provides support for Cloud Foundry’s GCP Service Broker. +Our Pub/Sub, Cloud Spanner, Storage, Stackdriver Trace and Cloud SQL MySQL and PostgreSQL starters are Cloud Foundry aware and retrieve properties like project ID, credentials, etc., that are used in auto configuration from the Cloud Foundry environment.

    In cases like Pub/Sub’s topic and subscription, or Storage’s bucket name, where those parameters are not used in auto configuration, you can fetch them using the VCAP mapping provided by Spring Boot. +For example, to retrieve the provisioned Pub/Sub topic, you can use the vcap.services.mypubsub.credentials.topic_name property from the application environment.

    [Note]Note

    If the same service is bound to the same application more than once, the auto configuration will not be able to choose among bindings and will not be activated for that service. +This includes both MySQL and PostgreSQL bindings to the same app.

    [Warning]Warning

    In order for the Cloud SQL integration to work in Cloud Foundry, auto-reconfiguration must be disabled. +You can do so using the cf set-env <APP> JBP_CONFIG_SPRING_AUTO_RECONFIGURATION '{enabled: false}' command. +Otherwise, Cloud Foundry will produce a DataSource with an invalid JDBC URL (i.e., jdbc:mysql://null/null).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__cloud_identity_aware_proxy_iap_authentication.html b/Greenwich.SR5/multi/multi__cloud_identity_aware_proxy_iap_authentication.html new file mode 100644 index 00000000..ff12dd8a --- /dev/null +++ b/Greenwich.SR5/multi/multi__cloud_identity_aware_proxy_iap_authentication.html @@ -0,0 +1,13 @@ + + + 166. Cloud Identity-Aware Proxy (IAP) Authentication

    166. Cloud Identity-Aware Proxy (IAP) Authentication

    Cloud Identity-Aware Proxy (IAP) provides a security layer over applications deployed to Google Cloud.

    The IAP starter uses Spring Security OAuth 2.0 Resource Server functionality to automatically extract user identity from the proxy-injected x-goog-iap-jwt-assertion HTTP header.

    The following claims are validated automatically:

    • Issue time
    • Expiration time
    • Issuer
    • Audience

    The audience ("aud") validation is automatically configured when the application is running on App Engine Standard or App Engine Flexible. +For other runtime environments, a custom audience must be provided through spring.cloud.gcp.security.iap.audience property. +The custom property, if specified, overrides the automatic App Engine audience detection.

    [Important]Important

    There is no automatic audience string configuration for Compute Engine or Kubernetes Engine. +To use the IAP starter on GCE/GKE, find the Audience string per instructions in the Verify the JWT payload guide, and specify it in the spring.cloud.gcp.security.iap.audience property. +Otherwise, the application will fail to start with No qualifying bean of type 'org.springframework.cloud.gcp.security.iap.AudienceProvider' available message.

    [Note]Note

    If you create a custom WebSecurityConfigurerAdapter, enable extracting user identity by adding .oauth2ResourceServer().jwt() configuration to the HttpSecurity object. + If no custom WebSecurityConfigurerAdapter is present, nothing needs to be done because Spring Boot will add this customization by default.

    Starter Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-security-iap</artifactId>
    +</dependency>

    Starter Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-security-iap'
    +}

    166.1 Configuration

    The following properties are available.

    [Caution]Caution

    Modifying registry, algorithm, and header properties might be useful for testing, but the defaults should not be changed in production.

    NameDescriptionRequiredDefault

    spring.cloud.gcp.security.iap.registry

    Link to JWK public key registry.

    true

    https://www.gstatic.com/iap/verify/public_key-jwk

    spring.cloud.gcp.security.iap.algorithm

    Encryption algorithm used to sign the JWK token.

    true

    ES256

    spring.cloud.gcp.security.iap.header

    Header from which to extract the JWK key.

    true

    x-goog-iap-jwt-assertion

    spring.cloud.gcp.security.iap.issuer

    JWK issuer to verify.

    true

    https://cloud.google.com/iap

    spring.cloud.gcp.security.iap.audience

    Custom JWK audience to verify.

    false on App Engine; true on GCE/GKE

     

    166.2 Sample

    A sample application is available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__cloud_memorystore_for_redis.html b/Greenwich.SR5/multi/multi__cloud_memorystore_for_redis.html new file mode 100644 index 00000000..b3ff1dcd --- /dev/null +++ b/Greenwich.SR5/multi/multi__cloud_memorystore_for_redis.html @@ -0,0 +1,15 @@ + + + 165. Cloud Memorystore for Redis

    165. Cloud Memorystore for Redis

    165.1 Spring Caching

    Cloud Memorystore for Redis provides a fully managed in-memory data store service. +Cloud Memorystore is compatible with the Redis protocol, allowing easy integration with Spring Caching.

    All you have to do is create a Cloud Memorystore instance and use its IP address in application.properties file as spring.redis.host property value. +Everything else is exactly the same as setting up redis-backed Spring caching.

    [Note]Note

    Memorystore instances and your application instances have to be located in the same region.

    In short, the following dependencies are needed:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-cache</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-data-redis</artifactId>
    +</dependency>

    And then you can use org.springframework.cache.annotation.Cacheable annotation for methods you’d like to be cached.

    @Cacheable("cache1")
    +public String hello(@PathVariable String name) {
    +    ....
    +}

    If you are interested in a detailed how-to guide, please check Spring Boot Caching using Cloud Memorystore codelab.

    Cloud Memorystore documentation can be found here.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__cloud_native_applications.html b/Greenwich.SR5/multi/multi__cloud_native_applications.html new file mode 100644 index 00000000..ec67bd43 --- /dev/null +++ b/Greenwich.SR5/multi/multi__cloud_native_applications.html @@ -0,0 +1,9 @@ + + + Part I. Cloud Native Applications

    Part I. Cloud Native Applications

    Cloud Native is a style of application development that encourages easy adoption of best practices in the areas of continuous delivery and value-driven development. +A related discipline is that of building 12-factor Applications, in which development practices are aligned with delivery and operations goals — for instance, by using declarative programming and management and monitoring. +Spring Cloud facilitates these styles of development in a number of specific ways. + The starting point is a set of features to which all components in a distributed system need easy access.

    Many of those features are covered by Spring Boot, on which Spring Cloud builds. Some more features are delivered by Spring Cloud as two libraries: Spring Cloud Context and Spring Cloud Commons. +Spring Cloud Context provides utilities and special services for the ApplicationContext of a Spring Cloud application (bootstrap context, encryption, refresh scope, and environment endpoints). Spring Cloud Commons is a set of abstractions and common classes used in different Spring Cloud implementations (such as Spring Cloud Netflix and Spring Cloud Consul).

    If you get an exception due to "Illegal key size" and you use Sun’s JDK, you need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. +See the following links for more information:

    Extract the files into the JDK/jre/lib/security folder for whichever version of JRE/JDK x64/x86 you use.

    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. +If you would like to contribute to this section of the documentation or if you find an error, you can find the source code and issue trackers for the project at github.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__configuration_2.html b/Greenwich.SR5/multi/multi__configuration_2.html new file mode 100644 index 00000000..53ec22e3 --- /dev/null +++ b/Greenwich.SR5/multi/multi__configuration_2.html @@ -0,0 +1,63 @@ + + + 119. Configuration

    119. Configuration

    Configuration for Spring Cloud Gateway is driven by a collection of RouteDefinitionLocators.

    RouteDefinitionLocator.java.  +

    public interface RouteDefinitionLocator {
    +	Flux<RouteDefinition> getRouteDefinitions();
    +}

    +

    By default, a PropertiesRouteDefinitionLocator loads properties using Spring Boot’s @ConfigurationProperties mechanism.

    The configuration examples above all use a shortcut notation that uses positional arguments rather than named ones. The two examples below are equivalent:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setstatus_route
    +        uri: https://example.org
    +        filters:
    +        - name: SetStatus
    +          args:
    +            status: 401
    +      - id: setstatusshortcut_route
    +        uri: https://example.org
    +        filters:
    +        - SetStatus=401

    +

    For some usages of the gateway, properties will be adequate, but some production use cases will benefit from loading configuration from an external source, such as a database. Future milestone versions will have RouteDefinitionLocator implementations based off of Spring Data Repositories such as: Redis, MongoDB and Cassandra.

    119.1 Fluent Java Routes API

    To allow for simple configuration in Java, there is a fluent API defined in the RouteLocatorBuilder bean.

    GatewaySampleApplication.java.  +

    // static imports from GatewayFilters and RoutePredicates
    +@Bean
    +public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) {
    +    return builder.routes()
    +            .route(r -> r.host("**.abc.org").and().path("/image/png")
    +                .filters(f ->
    +                        f.addResponseHeader("X-TestHeader", "foobar"))
    +                .uri("http://httpbin.org:80")
    +            )
    +            .route(r -> r.path("/image/webp")
    +                .filters(f ->
    +                        f.addResponseHeader("X-AnotherHeader", "baz"))
    +                .uri("http://httpbin.org:80")
    +            )
    +            .route(r -> r.order(-1)
    +                .host("**.throttle.org").and().path("/get")
    +                .filters(f -> f.filter(throttle.apply(1,
    +                        1,
    +                        10,
    +                        TimeUnit.SECONDS)))
    +                .uri("http://httpbin.org:80")
    +            )
    +            .build();
    +}

    +

    This style also allows for more custom predicate assertions. The predicates defined by RouteDefinitionLocator beans are combined using logical and. By using the fluent Java API, you can use the and(), or() and negate() operators on the Predicate class.

    119.2 DiscoveryClient Route Definition Locator

    The Gateway can be configured to create routes based on services registered with a DiscoveryClient compatible service registry.

    To enable this, set spring.cloud.gateway.discovery.locator.enabled=true and make sure a DiscoveryClient implementation is on the classpath and enabled (such as Netflix Eureka, Consul or Zookeeper).

    119.2.1 Configuring Predicates and Filters For DiscoveryClient Routes

    By default the Gateway defines a single predicate and filter for routes created via a DiscoveryClient.

    The default predicate is a path predicate defined with the pattern /serviceId/**, where serviceId is +the id of the service from the DiscoveryClient.

    The default filter is rewrite path filter with the regex /serviceId/(?<remaining>.*) and the replacement +/${remaining}. This just strips the service id from the path before the request is sent +downstream.

    If you would like to customize the predicates and/or filters used by the DiscoveryClient routes you can do so +by setting spring.cloud.gateway.discovery.locator.predicates[x] and spring.cloud.gateway.discovery.locator.filters[y]. +When doing so you need to make sure to include the default predicate and filter above, if you want to retain +that functionality. Below is an example of what this looks like.

    application.properties.  +

    spring.cloud.gateway.discovery.locator.predicates[0].name: Path
    +spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'"
    +spring.cloud.gateway.discovery.locator.predicates[1].name: Host
    +spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'"
    +spring.cloud.gateway.discovery.locator.filters[0].name: Hystrix
    +spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId
    +spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath
    +spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/(?<remaining>.*)'"
    +spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'"

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__configuration_options.html b/Greenwich.SR5/multi/multi__configuration_options.html new file mode 100644 index 00000000..afb809a4 --- /dev/null +++ b/Greenwich.SR5/multi/multi__configuration_options.html @@ -0,0 +1,127 @@ + + + 31. Configuration Options

    31. Configuration Options

    Spring Cloud Stream supports general configuration options as well as configuration for bindings and binders. +Some binders let additional binding properties support middleware-specific features.

    Configuration options can be provided to Spring Cloud Stream applications through any mechanism supported by Spring Boot. +This includes application arguments, environment variables, and YAML or .properties files.

    31.1 Binding Service Properties

    These properties are exposed via org.springframework.cloud.stream.config.BindingServiceProperties

    spring.cloud.stream.instanceCount

    The number of deployed instances of an application. +Must be set for partitioning on the producer side. Must be set on the consumer side when using RabbitMQ and with Kafka if autoRebalanceEnabled=false.

    Default: 1.

    spring.cloud.stream.instanceIndex
    The instance index of the application: A number from 0 to instanceCount - 1. +Used for partitioning with RabbitMQ and with Kafka if autoRebalanceEnabled=false. +Automatically set in Cloud Foundry to match the application’s instance index.
    spring.cloud.stream.dynamicDestinations

    A list of destinations that can be bound dynamically (for example, in a dynamic routing scenario). +If set, only listed destinations can be bound.

    Default: empty (letting any destination be bound).

    spring.cloud.stream.defaultBinder

    The default binder to use, if multiple binders are configured. +See Multiple Binders on the Classpath.

    Default: empty.

    spring.cloud.stream.overrideCloudConnectors

    This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. +If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). +When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). +The typical usage of this property is to be nested in a customized environment when connecting to multiple systems.

    Default: false.

    spring.cloud.stream.bindingRetryInterval

    The interval (in seconds) between retrying binding creation when, for example, the binder does not support late binding and the broker (for example, Apache Kafka) is down. +Set it to zero to treat such conditions as fatal, preventing the application from starting.

    Default: 30

    31.2 Binding Properties

    Binding properties are supplied by using the format of spring.cloud.stream.bindings.<channelName>.<property>=<value>. +The <channelName> represents the name of the channel being configured (for example, output for a Source).

    To avoid repetition, Spring Cloud Stream supports setting values for all channels, in the format of spring.cloud.stream.default.<property>=<value>.

    When it comes to avoiding repetitions for extended binding properties, this format should be used - spring.cloud.stream.<binder-type>.default.<producer|consumer>.<property>=<value>.

    In what follows, we indicate where we have omitted the spring.cloud.stream.bindings.<channelName>. prefix and focus just on the property name, with the understanding that the prefix ise included at runtime.

    31.2.1 Common Binding Properties

    These properties are exposed via org.springframework.cloud.stream.config.BindingProperties

    The following binding properties are available for both input and output bindings and must be prefixed with spring.cloud.stream.bindings.<channelName>. (for example, spring.cloud.stream.bindings.input.destination=ticktock).

    Default values can be set by using the spring.cloud.stream.default prefix (for example`spring.cloud.stream.default.contentType=application/json`).

    destination
    The target destination of a channel on the bound middleware (for example, the RabbitMQ exchange or Kafka topic). +If the channel is bound as a consumer, it could be bound to multiple destinations, and the destination names can be specified as comma-separated String values. +If not set, the channel name is used instead. +The default value of this property cannot be overridden.
    group

    The consumer group of the channel. +Applies only to inbound bindings. +See Consumer Groups.

    Default: null (indicating an anonymous consumer).

    contentType

    The content type of the channel. +See Chapter 32, Content Type Negotiation.

    Default: application/json.

    binder

    The binder used by this binding. +See Section 30.4, “Multiple Binders on the Classpath” for details.

    Default: null (the default binder is used, if it exists).

    31.2.2 Consumer Properties

    These properties are exposed via org.springframework.cloud.stream.binder.ConsumerProperties

    The following binding properties are available for input bindings only and must be prefixed with spring.cloud.stream.bindings.<channelName>.consumer. (for example, spring.cloud.stream.bindings.input.consumer.concurrency=3).

    Default values can be set by using the spring.cloud.stream.default.consumer prefix (for example, spring.cloud.stream.default.consumer.headerMode=none).

    concurrency

    The concurrency of the inbound consumer.

    Default: 1.

    partitioned

    Whether the consumer receives data from a partitioned producer.

    Default: false.

    headerMode

    When set to none, disables header parsing on input. +Effective only for messaging middleware that does not support message headers natively and requires header embedding. +This option is useful when consuming data from non-Spring Cloud Stream applications when native headers are not supported. +When set to headers, it uses the middleware’s native header mechanism. +When set to embeddedHeaders, it embeds headers into the message payload.

    Default: depends on the binder implementation.

    maxAttempts

    If processing fails, the number of attempts to process the message (including the first). +Set to 1 to disable retry.

    Default: 3.

    backOffInitialInterval

    The backoff initial interval on retry.

    Default: 1000.

    backOffMaxInterval

    The maximum backoff interval.

    Default: 10000.

    backOffMultiplier

    The backoff multiplier.

    Default: 2.0.

    defaultRetryable

    Whether exceptions thrown by the listener that are not listed in the retryableExceptions are retryable.

    Default: true.

    instanceIndex

    When set to a value greater than equal to zero, it allows customizing the instance index of this consumer (if different from spring.cloud.stream.instanceIndex). +When set to a negative value, it defaults to spring.cloud.stream.instanceIndex. +See Section 34.2, “Instance Index and Instance Count” for more information.

    Default: -1.

    instanceCount

    When set to a value greater than equal to zero, it allows customizing the instance count of this consumer (if different from spring.cloud.stream.instanceCount). +When set to a negative value, it defaults to spring.cloud.stream.instanceCount. +See Section 34.2, “Instance Index and Instance Count” for more information.

    Default: -1.

    retryableExceptions

    A map of Throwable class names in the key and a boolean in the value. +Specify those exceptions (and subclasses) that will or won’t be retried. +Also see defaultRetriable. +Example: spring.cloud.stream.bindings.input.consumer.retryable-exceptions.java.lang.IllegalStateException=false.

    Default: empty.

    useNativeDecoding

    When set to true, the inbound message is deserialized directly by the client library, which must be configured correspondingly (for example, setting an appropriate Kafka producer value deserializer). +When this configuration is being used, the inbound message unmarshalling is not based on the contentType of the binding. +When native decoding is used, it is the responsibility of the producer to use an appropriate encoder (for example, the Kafka producer value serializer) to serialize the outbound message. +Also, when native encoding and decoding is used, the headerMode=embeddedHeaders property is ignored and headers are not embedded in the message. +See the producer property useNativeEncoding.

    Default: false.

    31.2.3 Producer Properties

    These properties are exposed via org.springframework.cloud.stream.binder.ProducerProperties

    The following binding properties are available for output bindings only and must be prefixed with spring.cloud.stream.bindings.<channelName>.producer. (for example, spring.cloud.stream.bindings.input.producer.partitionKeyExpression=payload.id).

    Default values can be set by using the prefix spring.cloud.stream.default.producer (for example, spring.cloud.stream.default.producer.partitionKeyExpression=payload.id).

    partitionKeyExpression

    A SpEL expression that determines how to partition outbound data. +If set, or if partitionKeyExtractorClass is set, outbound data on this channel is partitioned. partitionCount must be set to a value greater than 1 to be effective. +Mutually exclusive with partitionKeyExtractorClass. +See Section 28.6, “Partitioning Support”.

    Default: null.

    partitionKeyExtractorClass

    A PartitionKeyExtractorStrategy implementation. +If set, or if partitionKeyExpression is set, outbound data on this channel is partitioned. partitionCount must be set to a value greater than 1 to be effective. +Mutually exclusive with partitionKeyExpression. +See Section 28.6, “Partitioning Support”.

    Default: null.

    partitionSelectorClass

    A PartitionSelectorStrategy implementation. +Mutually exclusive with partitionSelectorExpression. +If neither is set, the partition is selected as the hashCode(key) % partitionCount, where key is computed through either partitionKeyExpression or partitionKeyExtractorClass.

    Default: null.

    partitionSelectorExpression

    A SpEL expression for customizing partition selection. +Mutually exclusive with partitionSelectorClass. +If neither is set, the partition is selected as the hashCode(key) % partitionCount, where key is computed through either partitionKeyExpression or partitionKeyExtractorClass.

    Default: null.

    partitionCount

    The number of target partitions for the data, if partitioning is enabled. +Must be set to a value greater than 1 if the producer is partitioned. +On Kafka, it is interpreted as a hint. The larger of this and the partition count of the target topic is used instead.

    Default: 1.

    requiredGroups
    A comma-separated list of groups to which the producer must ensure message delivery even if they start after it has been created (for example, by pre-creating durable queues in RabbitMQ).
    headerMode

    When set to none, it disables header embedding on output. +It is effective only for messaging middleware that does not support message headers natively and requires header embedding. +This option is useful when producing data for non-Spring Cloud Stream applications when native headers are not supported. +When set to headers, it uses the middleware’s native header mechanism. +When set to embeddedHeaders, it embeds headers into the message payload.

    Default: Depends on the binder implementation.

    useNativeEncoding

    When set to true, the outbound message is serialized directly by the client library, which must be configured correspondingly (for example, setting an appropriate Kafka producer value serializer). +When this configuration is being used, the outbound message marshalling is not based on the contentType of the binding. +When native encoding is used, it is the responsibility of the consumer to use an appropriate decoder (for example, the Kafka consumer value de-serializer) to deserialize the inbound message. +Also, when native encoding and decoding is used, the headerMode=embeddedHeaders property is ignored and headers are not embedded in the message. +See the consumer property useNativeDecoding.

    Default: false.

    errorChannelEnabled

    When set to true, if the binder supports asynchroous send results, send failures are sent to an error channel for the destination. +See ??? for more information.

    Default: false.

    31.3 Using Dynamically Bound Destinations

    Besides the channels defined by using @EnableBinding, Spring Cloud Stream lets applications send messages to dynamically bound destinations. +This is useful, for example, when the target destination needs to be determined at runtime. +Applications can do so by using the BinderAwareChannelResolver bean, registered automatically by the @EnableBinding annotation.

    The 'spring.cloud.stream.dynamicDestinations' property can be used for restricting the dynamic destination names to a known set (whitelisting). +If this property is not set, any destination can be bound dynamically.

    The BinderAwareChannelResolver can be used directly, as shown in the following example of a REST controller using a path variable to decide the target channel:

    @EnableBinding
    +@Controller
    +public class SourceWithDynamicDestination {
    +
    +    @Autowired
    +    private BinderAwareChannelResolver resolver;
    +
    +    @RequestMapping(path = "/{target}", method = POST, consumes = "*/*")
    +    @ResponseStatus(HttpStatus.ACCEPTED)
    +    public void handleRequest(@RequestBody String body, @PathVariable("target") target,
    +           @RequestHeader(HttpHeaders.CONTENT_TYPE) Object contentType) {
    +        sendMessage(body, target, contentType);
    +    }
    +
    +    private void sendMessage(String body, String target, Object contentType) {
    +        resolver.resolveDestination(target).send(MessageBuilder.createMessage(body,
    +                new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType))));
    +    }
    +}

    Now consider what happens when we start the application on the default port (8080) and make the following requests with CURL:

    curl -H "Content-Type: application/json" -X POST -d "customer-1" http://localhost:8080/customers
    +
    +curl -H "Content-Type: application/json" -X POST -d "order-1" http://localhost:8080/orders

    The destinations, 'customers' and 'orders', are created in the broker (in the exchange for Rabbit or in the topic for Kafka) with names of 'customers' and 'orders', and the data is published to the appropriate destinations.

    The BinderAwareChannelResolver is a general-purpose Spring Integration DestinationResolver and can be injected in other components — for example, in a router using a SpEL expression based on the target field of an incoming JSON message. The following example includes a router that reads SpEL expressions:

    @EnableBinding
    +@Controller
    +public class SourceWithDynamicDestination {
    +
    +    @Autowired
    +    private BinderAwareChannelResolver resolver;
    +
    +
    +    @RequestMapping(path = "/", method = POST, consumes = "application/json")
    +    @ResponseStatus(HttpStatus.ACCEPTED)
    +    public void handleRequest(@RequestBody String body, @RequestHeader(HttpHeaders.CONTENT_TYPE) Object contentType) {
    +        sendMessage(body, contentType);
    +    }
    +
    +    private void sendMessage(Object body, Object contentType) {
    +        routerChannel().send(MessageBuilder.createMessage(body,
    +                new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType))));
    +    }
    +
    +    @Bean(name = "routerChannel")
    +    public MessageChannel routerChannel() {
    +        return new DirectChannel();
    +    }
    +
    +    @Bean
    +    @ServiceActivator(inputChannel = "routerChannel")
    +    public ExpressionEvaluatingRouter router() {
    +        ExpressionEvaluatingRouter router =
    +            new ExpressionEvaluatingRouter(new SpelExpressionParser().parseExpression("payload.target"));
    +        router.setDefaultOutputChannelName("default-output");
    +        router.setChannelResolver(resolver);
    +        return router;
    +    }
    +}

    The Router Sink Application uses this technique to create the destinations on-demand.

    If the channel names are known in advance, you can configure the producer properties as with any other destination. +Alternatively, if you register a NewBindingCallback<> bean, it is invoked just before the binding is created. +The callback takes the generic type of the extended producer properties used by the binder. +It has one method:

    void configure(String channelName, MessageChannel channel, ProducerProperties producerProperties,
    +        T extendedProducerProperties);

    The following example shows how to use the RabbitMQ binder:

    @Bean
    +public NewBindingCallback<RabbitProducerProperties> dynamicConfigurer() {
    +    return (name, channel, props, extended) -> {
    +        props.setRequiredGroups("bindThisQueue");
    +        extended.setQueueNameGroupOnly(true);
    +        extended.setAutoBindDlq(true);
    +        extended.setDeadLetterQueueName("myDLQ");
    +    };
    +}
    [Note]Note

    If you need to support dynamic destinations with multiple binder types, use Object for the generic type and cast the extended argument as needed.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__configuring_authentication_downstream_of_a_zuul_proxy.html b/Greenwich.SR5/multi/multi__configuring_authentication_downstream_of_a_zuul_proxy.html new file mode 100644 index 00000000..50ae7870 --- /dev/null +++ b/Greenwich.SR5/multi/multi__configuring_authentication_downstream_of_a_zuul_proxy.html @@ -0,0 +1,17 @@ + + + 83. Configuring Authentication Downstream of a Zuul Proxy

    83. Configuring Authentication Downstream of a Zuul Proxy

    You can control the authorization behaviour downstream of an +@EnableZuulProxy through the proxy.auth.* settings. Example:

    application.yml.  +

    proxy:
    +  auth:
    +    routes:
    +      customers: oauth2
    +      stores: passthru
    +      recommendations: none

    +

    In this example the "customers" service gets an OAuth2 token relay, +the "stores" service gets a passthrough (the authorization header is +just passed downstream), and the "recommendations" service has its +authorization header removed. The default behaviour is to do a token +relay if there is a token available, and passthru otherwise.

    See + +ProxyAuthenticationProperties for full details.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__configuring_route_predicate_factories_and_gateway_filter_factories.html b/Greenwich.SR5/multi/multi__configuring_route_predicate_factories_and_gateway_filter_factories.html new file mode 100644 index 00000000..1b9e0b3d --- /dev/null +++ b/Greenwich.SR5/multi/multi__configuring_route_predicate_factories_and_gateway_filter_factories.html @@ -0,0 +1,24 @@ + + + 113. Configuring Route Predicate Factories and Gateway Filter Factories

    113. Configuring Route Predicate Factories and Gateway Filter Factories

    There are two ways to configure predicates and filters: shortcuts and fully expanded arguments. Most examples below use the shortcut way.

    The name and argument names will be listed as code in the first sentance or two of the each section. The arguments are typically listed in the order that would be needed for the shortcut configuration.

    113.1 Shortcut Configuration

    Shortcut configuration is recognized by the filter name, followed by an equals sign (=), followed by argument values separated by commas (,).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: after_route
    +        uri: https://example.org
    +        predicates:
    +        - Cookie=mycookie,mycookievalue

    +

    The previous sample defines the Cookie Route Predicate Factory with two arguments, the cookie name, mycookie and the value to match mycookievalue.

    113.2 Fully Expanded Arguments

    Fully expanded arguments appear more like standard yaml configuration with name/value pairs. Typically, there will be a name key and an args key. The args key is a map of key value pairs to configure the predicate or filter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: after_route
    +        uri: https://example.org
    +        predicates:
    +        - name: Cookie
    +          args:
    +            name: mycookie
    +            regexp: mycookievalue

    +

    This is the full configuration of the shortcut configuration of the Cookie predicate shown above.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__contributing.html b/Greenwich.SR5/multi/multi__contributing.html new file mode 100644 index 00000000..7d923229 --- /dev/null +++ b/Greenwich.SR5/multi/multi__contributing.html @@ -0,0 +1,88 @@ + + + 150. Contributing

    150. Contributing

    Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below.

    150.1 Sign the Contributor License Agreement

    Before we accept a non-trivial patch or pull request we will need you to sign the +Contributor License Agreement. +Signing the contributor’s agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests.

    150.2 Code of Conduct

    This project adheres to the Contributor Covenant code of +conduct. By participating, you are expected to uphold this code. Please report +unacceptable behavior to spring-code-of-conduct@pivotal.io.

    150.3 Code Conventions and Housekeeping

    None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge.

    • Use the Spring Framework code format conventions. If you use Eclipse +you can import formatter settings using the +eclipse-code-formatter.xml file from the +Spring +Cloud Build project. If using IntelliJ, you can use the +Eclipse Code Formatter +Plugin to import the same file.
    • Make sure all new .java files to have a simple Javadoc class comment with at least an +@author tag identifying you, and preferably at least a paragraph on what the class is +for.
    • Add the ASF license header comment to all new .java files (copy from existing files +in the project)
    • Add yourself as an @author to the .java files that you modify substantially (more +than cosmetic changes).
    • Add some Javadocs and, if you change the namespace, some XSD doc elements.
    • A few unit tests would help a lot as well — someone has to do it.
    • If no-one else is using your branch, please rebase it against the current master (or +other target branch in the main project).
    • When writing a commit message please follow these conventions, +if you are fixing an existing issue please add Fixes gh-XXXX at the end of the commit +message (where XXXX is the issue number).

    150.4 Checkstyle

    Spring Cloud Build comes with a set of checkstyle rules. You can find them in the spring-cloud-build-tools module. The most notable files under the module are:

    spring-cloud-build-tools/.  +

    └── src
    +    ├── checkstyle
    +    │   └── checkstyle-suppressions.xml 1
    +    └── main
    +        └── resources
    +            ├── checkstyle-header.txt 2
    +            └── checkstyle.xml 3

    +

    3

    Default Checkstyle rules

    2

    File header setup

    1

    Default suppression rules

    150.4.1 Checkstyle configuration

    Checkstyle rules are disabled by default. To add checkstyle to your project just define the following properties and plugins.

    pom.xml.  +

    <properties>
    +<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError> 1
    +        <maven-checkstyle-plugin.failsOnViolation>true
    +        </maven-checkstyle-plugin.failsOnViolation> 2
    +        <maven-checkstyle-plugin.includeTestSourceDirectory>true
    +        </maven-checkstyle-plugin.includeTestSourceDirectory> 3
    +</properties>
    +
    +<build>
    +        <plugins>
    +            <plugin> 4
    +                <groupId>io.spring.javaformat</groupId>
    +                <artifactId>spring-javaformat-maven-plugin</artifactId>
    +            </plugin>
    +            <plugin> 5
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-checkstyle-plugin</artifactId>
    +            </plugin>
    +        </plugins>
    +
    +    <reporting>
    +        <plugins>
    +            <plugin> 6
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-checkstyle-plugin</artifactId>
    +            </plugin>
    +        </plugins>
    +    </reporting>
    +</build>

    +

    1

    Fails the build upon Checkstyle errors

    2

    Fails the build upon Checkstyle violations

    3

    Checkstyle analyzes also the test sources

    4

    Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules

    5 6

    Add checkstyle plugin to your build and reporting phases

    If you need to suppress some rules (e.g. line length needs to be longer), then it’s enough for you to define a file under ${project.root}/src/checkstyle/checkstyle-suppressions.xml with your suppressions. Example:

    projectRoot/src/checkstyle/checkstyle-suppresions.xml.  +

    <?xml version="1.0"?>
    +<!DOCTYPE suppressions PUBLIC
    +		"-//Puppy Crawl//DTD Suppressions 1.1//EN"
    +		"https://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
    +<suppressions>
    +	<suppress files=".*ConfigServerApplication\.java" checks="HideUtilityClassConstructor"/>
    +	<suppress files=".*ConfigClientWatch\.java" checks="LineLengthCheck"/>
    +</suppressions>

    +

    It’s advisable to copy the ${spring-cloud-build.rootFolder}/.editorconfig and ${spring-cloud-build.rootFolder}/.springformat to your project. That way, some default formatting rules will be applied. You can do so by running this script:

    $ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig
    +$ touch .springformat

    150.5 IDE setup

    150.5.1 Intellij IDEA

    In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. +The following files can be found in the Spring Cloud Build project.

    spring-cloud-build-tools/.  +

    └── src
    +    ├── checkstyle
    +    │   └── checkstyle-suppressions.xml 1
    +    └── main
    +        └── resources
    +            ├── checkstyle-header.txt 2
    +            ├── checkstyle.xml 3
    +            └── intellij
    +                ├── Intellij_Project_Defaults.xml 4
    +                └── Intellij_Spring_Boot_Java_Conventions.xml 5

    +

    3

    Default Checkstyle rules

    2

    File header setup

    1

    Default suppression rules

    4

    Project defaults for Intellij that apply most of Checkstyle rules

    5

    Project style conventions for Intellij that apply most of Checkstyle rules

    Figure 150.1. Code style

    Code style

    Go to FileSettingsEditorCode style. There click on the icon next to the Scheme section. There, click on the Import Scheme value and pick the Intellij IDEA code style XML option. Import the spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml file.

    Figure 150.2. Inspection profiles

    Code style

    Go to FileSettingsEditorInspections. There click on the icon next to the Profile section. There, click on the Import Profile and import the spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml file.

    Checkstyle. To have Intellij work with Checkstyle, you have to install the Checkstyle plugin. It’s advisable to also install the Assertions2Assertj to automatically convert the JUnit assertions

    Checkstyle

    Go to FileSettingsOther settingsCheckstyle. There click on the + icon in the Configuration file section. There, you’ll have to define where the checkstyle rules should be picked from. In the image above, we’ve picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build’s GitHub repository (e.g. for the checkstyle.xml : https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml). We need to provide the following variables:

    [Important]Important

    Remember to set the Scan Scope to All sources since we apply checkstyle rules for production and test sources.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__cors_configuration.html b/Greenwich.SR5/multi/multi__cors_configuration.html new file mode 100644 index 00000000..89ac6e3d --- /dev/null +++ b/Greenwich.SR5/multi/multi__cors_configuration.html @@ -0,0 +1,13 @@ + + + 121. CORS Configuration

    121. CORS Configuration

    The gateway can be configured to control CORS behavior. The "global" CORS configuration is a map of URL patterns to Spring Framework CorsConfiguration.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      globalcors:
    +        corsConfigurations:
    +          '[/**]':
    +            allowedOrigins: "https://docs.spring.io"
    +            allowedMethods:
    +            - GET

    +

    In the example above, CORS requests will be allowed from requests that originate from docs.spring.io for all GET requested paths.

    To provide the same CORS configuration to requests that are not handled by some gateway route predicate, set the property spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping equal to true. This is useful when trying to support CORS preflight requests and your route predicate doesn’t evalute to true because the http method is options.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__current_span.html b/Greenwich.SR5/multi/multi__current_span.html new file mode 100644 index 00000000..30827a7b --- /dev/null +++ b/Greenwich.SR5/multi/multi__current_span.html @@ -0,0 +1,19 @@ + + + 56. Current Span

    56. Current Span

    Brave supports a current span concept which represents the in-flight operation. +You can use Tracer.currentSpan() to add custom tags to a span and Tracer.nextSpan() to create a child of whatever is in-flight.

    [Important]Important

    In Sleuth, you can autowire the Tracer bean to retrieve the current span via +tracer.currentSpan() method. To retrieve the current context just call +tracer.currentSpan().context(). To get the current trace id as String +you can use the traceIdString() method like this: tracer.currentSpan().context().traceIdString().

    56.1 Setting a span in scope manually

    When writing new instrumentation, it is important to place a span you created in scope as the current span. +Not only does doing so let users access it with Tracer.currentSpan(), but it also allows customizations such as SLF4J MDC to see the current trace IDs.

    Tracer.withSpanInScope(Span) facilitates this and is most conveniently employed by using the try-with-resources idiom. +Whenever external code might be invoked (such as proceeding an interceptor or otherwise), place the span in scope, as shown in the following example:

    @Autowired Tracer tracer;
    +
    +try (SpanInScope ws = tracer.withSpanInScope(span)) {
    +  return inboundRequest.invoke();
    +} finally { // note the scope is independent of the span
    +  span.finish();
    +}

    In edge cases, you may need to clear the current span temporarily (for example, launching a task that should not be associated with the current request). To do tso, pass null to withSpanInScope, as shown in the following example:

    @Autowired Tracer tracer;
    +
    +try (SpanInScope cleared = tracer.withSpanInScope(null)) {
    +  startBackgroundThread();
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__current_tracing_component.html b/Greenwich.SR5/multi/multi__current_tracing_component.html new file mode 100644 index 00000000..f40a676d --- /dev/null +++ b/Greenwich.SR5/multi/multi__current_tracing_component.html @@ -0,0 +1,7 @@ + + + 55. Current Tracing Component

    55. Current Tracing Component

    Brave supports a current tracing component concept, which should only be used when you have no other way to get a reference. +This was made for JDBC connections, as they often initialize prior to the tracing component.

    The most recent tracing component instantiated is available through Tracing.current(). +You can also use Tracing.currentTracer() to get only the tracer. +If you use either of these methods, do not cache the result. +Instead, look them up each time you need them.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__customization.html b/Greenwich.SR5/multi/multi__customization.html new file mode 100644 index 00000000..7827674b --- /dev/null +++ b/Greenwich.SR5/multi/multi__customization.html @@ -0,0 +1,217 @@ + + + 95. Customization

    95. Customization

    [Important]Important

    This section is valid only for Groovy DSL

    You can customize the Spring Cloud Contract Verifier by extending the DSL, as shown in +the remainder of this section.

    95.1 Extending the DSL

    You can provide your own functions to the DSL. The key requirement for this feature is to +maintain the static compatibility. Later in this document, you can see examples of:

    • Creating a JAR with reusable classes.
    • Referencing of these classes in the DSLs.

    You can find the full example +here.

    95.1.1 Common JAR

    The following examples show three classes that can be reused in the DSLs.

    PatternUtils contains functions used by both the consumer and the producer.

    package com.example;
    +
    +import java.util.regex.Pattern;
    +
    +/**
    + * If you want to use {@link Pattern} directly in your tests
    + * then you can create a class resembling this one. It can
    + * contain all the {@link Pattern} you want to use in the DSL.
    + *
    + * <pre>
    + * {@code
    + * request {
    + *     body(
    + *         [ age: $(c(PatternUtils.oldEnough()))]
    + *     )
    + * }
    + * </pre>
    + *
    + * Notice that we're using both {@code $()} for dynamic values
    + * and {@code c()} for the consumer side.
    + *
    + * @author Marcin Grzejszczak
    + */
    +//tag::impl[]
    +public class PatternUtils {
    +
    +	public static String tooYoung() {
    +		//remove::start[]
    +		return "[0-1][0-9]";
    +		//remove::end[return]
    +	}
    +
    +	public static Pattern oldEnough() {
    +		//remove::start[]
    +		return Pattern.compile("[2-9][0-9]");
    +		//remove::end[return]
    +	}
    +
    +	/**
    +	 * Makes little sense but it's just an example ;)
    +	 */
    +	public static Pattern ok() {
    +		//remove::start[]
    +		return Pattern.compile("OK");
    +		//remove::end[return]
    +	}
    +}
    +//end::impl[]

    ConsumerUtils contains functions used by the consumer.

    package com.example;
    +
    +import org.springframework.cloud.contract.spec.internal.ClientDslProperty;
    +
    +/**
    + * DSL Properties passed to the DSL from the consumer's perspective.
    + * That means that on the input side {@code Request} for HTTP
    + * or {@code Input} for messaging you can have a regular expression.
    + * On the {@code Response} for HTTP or {@code Output} for messaging
    + * you have to have a concrete value.
    + *
    + * @author Marcin Grzejszczak
    + */
    +//tag::impl[]
    +public class ConsumerUtils {
    +	/**
    +	 * Consumer side property. By using the {@link ClientDslProperty}
    +	 * you can omit most of boilerplate code from the perspective
    +	 * of dynamic values. Example
    +	 *
    +	 * <pre>
    +	 * {@code
    +	 * request {
    +	 *     body(
    +	 *         [ age: $(ConsumerUtils.oldEnough())]
    +	 *     )
    +	 * }
    +	 * </pre>
    +	 *
    +	 * That way it's in the implementation that we decide what value we will pass to the consumer
    +	 * and which one to the producer.
    +	 *
    +	 * @author Marcin Grzejszczak
    +	 */
    +	public static ClientDslProperty oldEnough() {
    +		//remove::start[]
    +		// this example is not the best one and
    +		// theoretically you could just pass the regex instead of `ServerDslProperty` but
    +		// it's just to show some new tricks :)
    +		return new ClientDslProperty(PatternUtils.oldEnough(), 40);
    +		//remove::end[return]
    +	}
    +
    +}
    +//end::impl[]

    ProducerUtils contains functions used by the producer.

    package com.example;
    +
    +import org.springframework.cloud.contract.spec.internal.ServerDslProperty;
    +
    +/**
    + * DSL Properties passed to the DSL from the producer's perspective.
    + * That means that on the input side {@code Request} for HTTP
    + * or {@code Input} for messaging you have to have a concrete value.
    + * On the {@code Response} for HTTP or {@code Output} for messaging
    + * you can have a regular expression.
    + *
    + * @author Marcin Grzejszczak
    + */
    +//tag::impl[]
    +public class ProducerUtils {
    +
    +	/**
    +	 * Producer side property. By using the {@link ProducerUtils}
    +	 * you can omit most of boilerplate code from the perspective
    +	 * of dynamic values. Example
    +	 *
    +	 * <pre>
    +	 * {@code
    +	 * response {
    +	 *     body(
    +	 *         [ status: $(ProducerUtils.ok())]
    +	 *     )
    +	 * }
    +	 * </pre>
    +	 *
    +	 * That way it's in the implementation that we decide what value we will pass to the consumer
    +	 * and which one to the producer.
    +	 */
    +	public static ServerDslProperty ok() {
    +		// this example is not the best one and
    +		// theoretically you could just pass the regex instead of `ServerDslProperty` but
    +		// it's just to show some new tricks :)
    +		return new ServerDslProperty( PatternUtils.ok(), "OK");
    +	}
    +}
    +//end::impl[]

    95.1.2 Adding the Dependency to the Project

    In order for the plugins and IDE to be able to reference the common JAR classes, you need +to pass the dependency to your project.

    95.1.3 Test the Dependency in the Project’s Dependencies

    First, add the common jar dependency as a test dependency. Because your contracts files +are available on the test resources path, the common jar classes automatically become +visible in your Groovy files. The following examples show how to test the dependency:

    Maven.  +

    <dependency>
    +	<groupId>com.example</groupId>
    +	<artifactId>beer-common</artifactId>
    +	<version>${project.version}</version>
    +	<scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile("com.example:beer-common:0.0.1.BUILD-SNAPSHOT")

    +

    95.1.4 Test a Dependency in the Plugin’s Dependencies

    Now, you must add the dependency for the plugin to reuse at runtime, as shown in the +following example:

    Maven.  +

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example</packageWithBaseClasses>
    +		<baseClassMappings>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*intoxication.*</contractPackageRegex>
    +				<baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN>
    +			</baseClassMapping>
    +		</baseClassMappings>
    +	</configuration>
    +	<dependencies>
    +		<dependency>
    +			<groupId>com.example</groupId>
    +			<artifactId>beer-common</artifactId>
    +			<version>${project.version}</version>
    +			<scope>compile</scope>
    +		</dependency>
    +	</dependencies>
    +</plugin>

    +

    Gradle.  +

    classpath "com.example:beer-common:0.0.1.BUILD-SNAPSHOT"

    +

    95.1.5 Referencing classes in DSLs

    You can now reference your classes in your DSL, as shown in the following example:

    package contracts.beer.rest
    +
    +import com.example.ConsumerUtils
    +import com.example.ProducerUtils
    +import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +	description("""
    +Represents a successful scenario of getting a beer
    +
    +```
    +given:
    +	client is old enough
    +when:
    +	he applies for a beer
    +then:
    +	we'll grant him the beer
    +```
    +
    +""")
    +	request {
    +		method 'POST'
    +		url '/check'
    +		body(
    +				age: $(ConsumerUtils.oldEnough())
    +		)
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +	response {
    +		status 200
    +		body("""
    +			{
    +				"status": "${value(ProducerUtils.ok())}"
    +			}
    +			""")
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +}
    [Important]Important

    You can set the Spring Cloud Contract plugin up by setting convertToYaml to true. That way you will NOT have to add the dependency with the extended functionality to the consumer side, since the consumer side will be using YAML contracts instead of Groovy ones.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__customizations.html b/Greenwich.SR5/multi/multi__customizations.html new file mode 100644 index 00000000..0671bb60 --- /dev/null +++ b/Greenwich.SR5/multi/multi__customizations.html @@ -0,0 +1,101 @@ + + + 61. Customizations

    61. Customizations

    61.1 Customizers

    With Brave 5.7 you have various options of providing customizers for your project. Brave ships with

    • TracingCustomizer - allows configuration plugins to collaborate on building an instance of Tracing.
    • CurrentTraceContextCustomizer - allows configuration plugins to collaborate on building an instance of CurrentTraceContext.
    • ExtraFieldCustomizer - allows configuration plugins to collaborate on building an instance of ExtraFieldPropagation.Factory.

    Sleuth will search for beans of those types and automatically apply customizations.

    61.2 HTTP

    If a customization of client / server parsing of the HTTP related spans is +required, just register a bean of type brave.http.HttpClientParser or +brave.http.HttpServerParser. If client /server sampling is required, just +register a bean of type brave.sampler.SamplerFunction<HttpRequest> and name +the bean sleuthHttpClientSampler for client sampler and +sleuthHttpServerSampler for server sampler.

    For your convenience the @HttpClientSampler and @HttpServerSampler +annotations can be used to inject the proper beans or to reference the bean +names via their static String NAME fields.

    Check out Brave’s code to see an example of how to make a path-based sampler +https://github.com/openzipkin/brave/tree/master/instrumentation/http#sampling-policy

    If you want to completely rewrite the HttpTracing bean you can use the SkipPatternProvider +interface to retrieve the URL Pattern for spans that should be not sampled. Below you can see +an example of usage of SkipPatternProvider inside a server side, Sampler<HttpRequest>.

    @Configuration
    +class Config {
    +  @Bean(name = HttpServerSampler.NAME)
    +  SamplerFunction<HttpRequest> myHttpSampler(SkipPatternProvider provider) {
    +  	Pattern pattern = provider.skipPattern();
    +  	return request -> {
    +  		String url = request.path();
    +  		boolean shouldSkip = pattern.matcher(url).matches();
    +  		if (shouldSkip) {
    +  			return false;
    +  		}
    +  		return null;
    +  	};
    +  }
    +}

    61.3 TracingFilter

    You can also modify the behavior of the TracingFilter, which is the component that is responsible for processing the input HTTP request and adding tags basing on the HTTP response. +You can customize the tags or modify the response headers by registering your own instance of the TracingFilter bean.

    In the following example, we register the TracingFilter bean, add the ZIPKIN-TRACE-ID response header containing the current Span’s trace id, and add a tag with key custom and a value tag to the span.

    @Component
    +@Order(TraceWebServletAutoConfiguration.TRACING_FILTER_ORDER + 1)
    +class MyFilter extends GenericFilterBean {
    +
    +	private final Tracer tracer;
    +
    +	MyFilter(Tracer tracer) {
    +		this.tracer = tracer;
    +	}
    +
    +	@Override
    +	public void doFilter(ServletRequest request, ServletResponse response,
    +			FilterChain chain) throws IOException, ServletException {
    +		Span currentSpan = this.tracer.currentSpan();
    +		if (currentSpan == null) {
    +			chain.doFilter(request, response);
    +			return;
    +		}
    +		// for readability we're returning trace id in a hex form
    +		((HttpServletResponse) response).addHeader("ZIPKIN-TRACE-ID",
    +				currentSpan.context().traceIdString());
    +		// we can also add some custom tags
    +		currentSpan.tag("custom", "tag");
    +		chain.doFilter(request, response);
    +	}
    +
    +}

    61.4 RPC

    Sleuth automatically configures the RpcTracing bean which serves as a +foundation for RPC instrumentation such as gRPC or Dubbo.

    If a customization of client / server sampling of the RPC traces is required, +just register a bean of type brave.sampler.SamplerFunction<RpcRequest> and +name the bean sleuthRpcClientSampler for client sampler and +sleuthRpcServerSampler for server sampler.

    For your convenience the @RpcClientSampler and @RpcServerSampler +annotations can be used to inject the proper beans or to reference the bean +names via their static String NAME fields.

    Ex. Here’s a sampler that traces 100 "GetUserToken" server requests per second. +This doesn’t start new traces for requests to the health check service. Other +requests will use the global sampling configuration.

    @Configuration
    +class Config {
    +  @Bean(name = RpcServerSampler.NAME)
    +  SamplerFunction<RpcRequest> myRpcSampler() {
    +  	Matcher<RpcRequest> userAuth = and(serviceEquals("users.UserService"),
    +  			methodEquals("GetUserToken"));
    +  	return RpcRuleSampler.newBuilder()
    +  			.putRule(serviceEquals("grpc.health.v1.Health"), Sampler.NEVER_SAMPLE)
    +  			.putRule(userAuth, RateLimitingSampler.create(100)).build();
    +  }
    +}

    For more, see https://github.com/openzipkin/brave/tree/master/instrumentation/rpc#sampling-policy

    61.5 Custom service name

    By default, Sleuth assumes that, when you send a span to Zipkin, you want the span’s service name to be equal to the value of the spring.application.name property. +That is not always the case, though. +There are situations in which you want to explicitly provide a different service name for all spans coming from your application. +To achieve that, you can pass the following property to your application to override that value (the example is for a service named myService):

    spring.zipkin.service.name: myService

    61.6 Customization of Reported Spans

    Before reporting spans (for example, to Zipkin) you may want to modify that span in some way. +You can do so by using the FinishedSpanHandler interface.

    In Sleuth, we generate spans with a fixed name. +Some users want to modify the name depending on values of tags. +You can implement the FinishedSpanHandler interface to alter that name.

    The following example shows how to register two beans that implement FinishedSpanHandler:

    @Bean
    +FinishedSpanHandler handlerOne() {
    +	return new FinishedSpanHandler() {
    +		@Override
    +		public boolean handle(TraceContext traceContext, MutableSpan span) {
    +			span.name("foo");
    +			return true; // keep this span
    +		}
    +	};
    +}
    +
    +@Bean
    +FinishedSpanHandler handlerTwo() {
    +	return new FinishedSpanHandler() {
    +		@Override
    +		public boolean handle(TraceContext traceContext, MutableSpan span) {
    +			span.name(span.name() + " bar");
    +			return true; // keep this span
    +		}
    +	};
    +}

    The preceding example results in changing the name of the reported span to foo bar, just before it gets reported (for example, to Zipkin).

    61.7 Host Locator

    [Important]Important

    This section is about defining host from service discovery. +It is NOT about finding Zipkin through service discovery.

    To define the host that corresponds to a particular span, we need to resolve the host name and port. +The default approach is to take these values from server properties. +If those are not set, we try to retrieve the host name from the network interfaces.

    If you have the discovery client enabled and prefer to retrieve the host address from the registered instance in a service registry, you have to set the spring.zipkin.locator.discovery.enabled property (it is applicable for both HTTP-based and Stream-based span reporting), as follows:

    spring.zipkin.locator.discovery.enabled: true
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__customizing_the_message_broker.html b/Greenwich.SR5/multi/multi__customizing_the_message_broker.html new file mode 100644 index 00000000..f8f7ee26 --- /dev/null +++ b/Greenwich.SR5/multi/multi__customizing_the_message_broker.html @@ -0,0 +1,13 @@ + + + 47. Customizing the Message Broker

    47. Customizing the Message Broker

    Spring Cloud Bus uses Spring Cloud Stream to +broadcast the messages. So, to get messages to flow, you need only include the binder +implementation of your choice in the classpath. There are convenient starters for the bus +with AMQP (RabbitMQ) and Kafka (spring-cloud-starter-bus-[amqp|kafka]). Generally +speaking, Spring Cloud Stream relies on Spring Boot autoconfiguration conventions for +configuring middleware. For instance, the AMQP broker address can be changed with +spring.rabbitmq.* configuration properties. Spring Cloud Bus has a handful of +native configuration properties in spring.cloud.bus.* (for example, +spring.cloud.bus.destination is the name of the topic to use as the external +middleware). Normally, the defaults suffice.

    To learn more about how to customize the message broker settings, consult the Spring Cloud +Stream documentation.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__dependency_management.html b/Greenwich.SR5/multi/multi__dependency_management.html new file mode 100644 index 00000000..7cf3ceb3 --- /dev/null +++ b/Greenwich.SR5/multi/multi__dependency_management.html @@ -0,0 +1,15 @@ + + + 152. Dependency Management

    152. Dependency Management

    The Spring Cloud GCP Bill of Materials (BOM) contains the versions of all the dependencies it uses.

    If you’re a Maven user, adding the following to your pom.xml file will allow you to not specify any Spring Cloud GCP dependency versions. +Instead, the version of the BOM you’re using determines the versions of the used dependencies.

    <dependencyManagement>
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-gcp-dependencies</artifactId>
    +            <version>{project-version}</version>
    +            <type>pom</type>
    +            <scope>import</scope>
    +        </dependency>
    +    </dependencies>
    +</dependencyManagement>

    In the following sections, it will be assumed you are using the Spring Cloud GCP BOM and the dependency snippets will not contain versions.

    Gradle users can achieve the same kind of BOM experience using Spring’s dependency-management-plugin Gradle plugin. +For simplicity, the Gradle dependency snippets in the remainder of this document will also omit their versions.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__deploying_a_packaged_function.html b/Greenwich.SR5/multi/multi__deploying_a_packaged_function.html new file mode 100644 index 00000000..cfa1201e --- /dev/null +++ b/Greenwich.SR5/multi/multi__deploying_a_packaged_function.html @@ -0,0 +1,3 @@ + + + 132. Deploying a Packaged Function

    132. Deploying a Packaged Function

    Spring Cloud Function provides a "deployer" library that allows you to launch a jar file (or exploded archive, or set of jar files) with an isolated class loader and expose the functions defined in it. This is quite a powerful tool that would allow you to, for instance, adapt a function to a range of different input-output adapters without changing the target jar file. Serverless platforms often have this kind of feature built in, so you could see it as a building block for a function invoker in such a platform (indeed the Riff Java function invoker uses this library).

    The standard entry point of the API is the Spring configuration annotation @EnableFunctionDeployer. If that is used in a Spring Boot application the deployer kicks in and looks for some configuration to tell it where to find the function jar. At a minimum the user has to provide a function.location which is a URL or resource location for the archive containing the functions. It can optionally use a maven: prefix to locate the artifact via a dependency lookup (see FunctionProperties for complete details). A Spring Boot application is bootstrapped from the jar file, using the MANIFEST.MF to locate a start class, so that a standard Spring Boot fat jar works well, for example. If the target jar can be launched successfully then the result is a function registered in the main application’s FunctionCatalog. The registered function can be applied by code in the main application, even though it was created in an isolated class loader (by deault).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__developer_guide.html b/Greenwich.SR5/multi/multi__developer_guide.html new file mode 100644 index 00000000..660395fa --- /dev/null +++ b/Greenwich.SR5/multi/multi__developer_guide.html @@ -0,0 +1,98 @@ + + + 124. Developer Guide

    124. Developer Guide

    These are basic guides to writing some custom components of the gateway.

    124.1 Writing Custom Route Predicate Factories

    In order to write a Route Predicate you will need to implement RoutePredicateFactory. There is an abstract class called AbstractRoutePredicateFactory which you can extend.

    MyRoutePredicateFactory.java.  +

    public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {
    +
    +    public MyRoutePredicateFactory() {
    +        super(Config.class);
    +    }
    +
    +    @Override
    +    public Predicate<ServerWebExchange> apply(Config config) {
    +        // grab configuration from Config object
    +        return exchange -> {
    +            //grab the request
    +            ServerHttpRequest request = exchange.getRequest();
    +            //take information from the request to see if it
    +            //matches configuration.
    +            return matches(config, request);
    +        };
    +    }
    +
    +    public static class Config {
    +        //Put the configuration properties for your filter here
    +    }
    +
    +}

    +

    124.2 Writing Custom GatewayFilter Factories

    In order to write a GatewayFilter you will need to implement GatewayFilterFactory. There is an abstract class called AbstractGatewayFilterFactory which you can extend.

    PreGatewayFilterFactory.java.  +

    public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory<PreGatewayFilterFactory.Config> {
    +
    +	public PreGatewayFilterFactory() {
    +		super(Config.class);
    +	}
    +
    +	@Override
    +	public GatewayFilter apply(Config config) {
    +		// grab configuration from Config object
    +		return (exchange, chain) -> {
    +			//If you want to build a "pre" filter you need to manipulate the
    +			//request before calling chain.filter
    +			ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
    +			//use builder to manipulate the request
    +			return chain.filter(exchange.mutate().request(request).build());
    +		};
    +	}
    +
    +	public static class Config {
    +		//Put the configuration properties for your filter here
    +	}
    +
    +}

    +

    PostGatewayFilterFactory.java.  +

    public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory<PostGatewayFilterFactory.Config> {
    +
    +	public PostGatewayFilterFactory() {
    +		super(Config.class);
    +	}
    +
    +	@Override
    +	public GatewayFilter apply(Config config) {
    +		// grab configuration from Config object
    +		return (exchange, chain) -> {
    +			return chain.filter(exchange).then(Mono.fromRunnable(() -> {
    +				ServerHttpResponse response = exchange.getResponse();
    +				//Manipulate the response in some way
    +			}));
    +		};
    +	}
    +
    +	public static class Config {
    +		//Put the configuration properties for your filter here
    +	}
    +
    +}

    +

    124.3 Writing Custom Global Filters

    In order to write a custom global filter, you will need to implement GlobalFilter interface. This will apply the filter to all requests.

    Example of how to set up a Global Pre and Post filter, respectively

    @Bean
    +public GlobalFilter customGlobalFilter() {
    +    return (exchange, chain) -> exchange.getPrincipal()
    +        .map(Principal::getName)
    +        .defaultIfEmpty("Default User")
    +        .map(userName -> {
    +          //adds header to proxied request
    +          exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build();
    +          return exchange;
    +        })
    +        .flatMap(chain::filter);
    +}
    +
    +@Bean
    +public GlobalFilter customGlobalPostFilter() {
    +    return (exchange, chain) -> chain.filter(exchange)
    +        .then(Mono.just(exchange))
    +        .map(serverWebExchange -> {
    +          //adds header to response
    +          serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER",
    +              HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked": "It did not work");
    +          return serverWebExchange;
    +        })
    +        .then();
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__discovery.html b/Greenwich.SR5/multi/multi__discovery.html new file mode 100644 index 00000000..a0ed5cd8 --- /dev/null +++ b/Greenwich.SR5/multi/multi__discovery.html @@ -0,0 +1,22 @@ + + + 84. Discovery

    84. Discovery

    Here’s a Spring Cloud app with Cloud Foundry discovery:

    app.groovy.  +

    @Grab('org.springframework.cloud:spring-cloud-cloudfoundry')
    +@RestController
    +@EnableDiscoveryClient
    +class Application {
    +
    +  @Autowired
    +  DiscoveryClient client
    +
    +  @RequestMapping('/')
    +  String home() {
    +    'Hello from ' + client.getLocalServiceInstance()
    +  }
    +
    +}

    +

    If you run it without any service bindings:

    $ spring jar app.jar app.groovy
    +$ cf push -p app.jar

    It will show its app name in the home page.

    The DiscoveryClient can lists all the apps in a space, according to +the credentials it is authenticated with, where the space defaults to +the one the client is running in (if any). If neither org nor space +are configured, they default per the user’s profile in Cloud Foundry.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__discoveryclient_for_kubernetes.html b/Greenwich.SR5/multi/multi__discoveryclient_for_kubernetes.html new file mode 100644 index 00000000..472e0e13 --- /dev/null +++ b/Greenwich.SR5/multi/multi__discoveryclient_for_kubernetes.html @@ -0,0 +1,21 @@ + + + 138. DiscoveryClient for Kubernetes

    138. DiscoveryClient for Kubernetes

    This project provides an implementation of Discovery Client +for Kubernetes. +This client lets you query Kubernetes endpoints (see services) by name. +A service is typically exposed by the Kubernetes API server as a collection of endpoints that represent http and https addresses and that a client can +access from a Spring Boot application running as a pod. This discovery feature is also used by the Spring Cloud Kubernetes Ribbon project +to fetch the list of the endpoints defined for an application to be load balanced.

    This is something that you get for free by adding the following dependency inside your project:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes</artifactId>
    +</dependency>

    To enable loading of the DiscoveryClient, add @EnableDiscoveryClient to the according configuration or application class, as the following example shows:

    @SpringBootApplication
    +@EnableDiscoveryClient
    +public class Application {
    +  public static void main(String[] args) {
    +    SpringApplication.run(Application.class, args);
    +  }
    +}

    Then you can inject the client in your code simply by autowiring it, as the following example shows:

    @Autowired
    +private DiscoveryClient discoveryClient;

    You can choose to enable DiscoveryClient from all namespaces by setting the following property in application.properties:

    spring.cloud.kubernetes.discovery.all-namespaces=true

    If, for any reason, you need to disable the DiscoveryClient, you can set the following property in application.properties:

    spring.cloud.kubernetes.discovery.enabled=false

    Some Spring Cloud components use the DiscoveryClient in order to obtain information about the local service instance. For +this to work, you need to align the Kubernetes service name with the spring.application.name property.

    [Note]Note

    spring.application.name has no effect as far as the name registered for the application within Kubernetes

    Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the +DiscoveryClient implementation accordingly. In order to enable this functionality you need to add +@EnableScheduling on a configuration class in your application.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__dynamic_compilation.html b/Greenwich.SR5/multi/multi__dynamic_compilation.html new file mode 100644 index 00000000..7ae5f9a3 --- /dev/null +++ b/Greenwich.SR5/multi/multi__dynamic_compilation.html @@ -0,0 +1,23 @@ + + + 134. Dynamic Compilation

    134. Dynamic Compilation

    There is a sample app that uses the function compiler to create a +function from a configuration property. The vanilla "function-sample" +also has that feature. And there are some scripts that you can run to +see the compilation happening at run time. To run these examples, +change into the scripts directory:

    cd scripts

    Also, start a RabbitMQ server locally (e.g. execute rabbitmq-server).

    Start the Function Registry Service:

    ./function-registry.sh

    Register a Function:

    ./registerFunction.sh -n uppercase -f "f->f.map(s->s.toString().toUpperCase())"

    Run a REST Microservice using that Function:

    ./web.sh -f uppercase -p 9000
    +curl -H "Content-Type: text/plain" -H "Accept: text/plain" localhost:9000/uppercase -d foo

    Register a Supplier:

    ./registerSupplier.sh -n words -f "()->Flux.just(\"foo\",\"bar\")"

    Run a REST Microservice using that Supplier:

    ./web.sh -s words -p 9001
    +curl -H "Accept: application/json" localhost:9001/words

    Register a Consumer:

    ./registerConsumer.sh -n print -t String -f "System.out::println"

    Run a REST Microservice using that Consumer:

    ./web.sh -c print -p 9002
    +curl -X POST -H "Content-Type: text/plain" -d foo localhost:9002/print

    Run Stream Processing Microservices:

    First register a streaming words supplier:

    ./registerSupplier.sh -n wordstream -f "()->Flux.interval(Duration.ofMillis(1000)).map(i->\"message-\"+i)"

    Then start the source (supplier), processor (function), and sink (consumer) apps +(in reverse order):

    ./stream.sh -p 9103 -i uppercaseWords -c print
    +./stream.sh -p 9102 -i words -f uppercase -o uppercaseWords
    +./stream.sh -p 9101 -s wordstream -o words

    The output will appear in the console of the sink app (one message per second, converted to uppercase):

    MESSAGE-0
    +MESSAGE-1
    +MESSAGE-2
    +MESSAGE-3
    +MESSAGE-4
    +MESSAGE-5
    +MESSAGE-6
    +MESSAGE-7
    +MESSAGE-8
    +MESSAGE-9
    +...
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__embedding_the_config_server.html b/Greenwich.SR5/multi/multi__embedding_the_config_server.html new file mode 100644 index 00000000..3d2d4c3f --- /dev/null +++ b/Greenwich.SR5/multi/multi__embedding_the_config_server.html @@ -0,0 +1,26 @@ + + + 8. Embedding the Config Server

    8. Embedding the Config Server

    The Config Server runs best as a standalone application. +However, if need be, you can embed it in another application. +To do so, use the @EnableConfigServer annotation. +An optional property named spring.cloud.config.server.bootstrap can be useful in this case. +It is a flag to indicate whether the server should configure itself from its own remote repository. +By default, the flag is off, because it can delay startup. +However, when embedded in another application, it makes sense to initialize the same way as any other application. +When setting spring.cloud.config.server.bootstrap to true you must also use a composite environment repository configuration. +For example

    spring:
    +  application:
    +    name: configserver
    +  profiles:
    +    active: composite
    +  cloud:
    +    config:
    +      server:
    +        composite:
    +          - type: native
    +            search-locations: ${HOME}/Desktop/config
    +        bootstrap: true
    [Note]Note

    If you use the bootstrap flag, the config server needs to have its name and repository URI configured in bootstrap.yml.

    To change the location of the server endpoints, you can (optionally) set spring.cloud.config.server.prefix (for example, /config), to serve the resources under a prefix. +The prefix should start but not end with a /. +It is applied to the @RequestMappings in the Config Server (that is, underneath the Spring Boot server.servletPath and server.contextPath prefixes).

    If you want to read the configuration for an application directly from the backend repository (instead of from the config server), you +basically want an embedded config server with no endpoints. +You can switch off the endpoints entirely by not using the @EnableConfigServer annotation (set spring.cloud.config.server.bootstrap=true).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__examples_2.html b/Greenwich.SR5/multi/multi__examples_2.html new file mode 100644 index 00000000..91483de0 --- /dev/null +++ b/Greenwich.SR5/multi/multi__examples_2.html @@ -0,0 +1,5 @@ + + + 147. Examples

    147. Examples

    Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by +following the Spring Cloud interfaces.

    In your applications, you need to add the spring-cloud-kubernetes-discovery dependency to your classpath and remove any other dependency that contains a DiscoveryClient implementation (that is, a Eureka discovery client). +The same applies for PropertySourceLocator, where you need to add to the classpath the spring-cloud-kubernetes-config and remove any other dependency that contains a PropertySourceLocator implementation (that is, a configuration server client).

    The following projects highlight the usage of these dependencies and demonstrate how you can use these libraries from any Spring Boot application:

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__external_configuration_archaius.html b/Greenwich.SR5/multi/multi__external_configuration_archaius.html new file mode 100644 index 00000000..d61dd8ca --- /dev/null +++ b/Greenwich.SR5/multi/multi__external_configuration_archaius.html @@ -0,0 +1,20 @@ + + + 17. External Configuration: Archaius

    17. External Configuration: Archaius

    Archaius is the Netflix client-side configuration library. +It is the library used by all of the Netflix OSS components for configuration. +Archaius is an extension of the Apache Commons Configuration project. +It allows updates to configuration by either polling a source for changes or by letting a source push changes to the client. +Archaius uses Dynamic<Type>Property classes as handles to properties, as shown in the following example:

    Archaius Example.  +

    class ArchaiusTest {
    +    DynamicStringProperty myprop = DynamicPropertyFactory
    +            .getInstance()
    +            .getStringProperty("my.prop");
    +
    +    void doSomething() {
    +        OtherClass.someMethod(myprop.get());
    +    }
    +}

    +

    Archaius has its own set of configuration files and loading priorities. +Spring applications should generally not use Archaius directly, but the need to configure the Netflix tools natively remains. +Spring Cloud has a Spring Environment Bridge so that Archaius can read properties from the Spring Environment. +This bridge allows Spring Boot projects to use the normal configuration toolchain while letting them configure the Netflix tools as documented (for the most part).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__features.html b/Greenwich.SR5/multi/multi__features.html new file mode 100644 index 00000000..00fe71c4 --- /dev/null +++ b/Greenwich.SR5/multi/multi__features.html @@ -0,0 +1,4 @@ + + + 1. Features

    1. Features

    Spring Cloud focuses on providing good out of box experience for typical use cases +and extensibility mechanism to cover others.

    • Distributed/versioned configuration
    • Service registration and discovery
    • Routing
    • Service-to-service calls
    • Load balancing
    • Circuit Breakers
    • Distributed messaging
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__features_2.html b/Greenwich.SR5/multi/multi__features_2.html new file mode 100644 index 00000000..ce823cec --- /dev/null +++ b/Greenwich.SR5/multi/multi__features_2.html @@ -0,0 +1,139 @@ + + + 52. Features

    52. Features

    • Adds trace and span IDs to the Slf4J MDC, so you can extract all the logs from a given trace or span in a log aggregator, as shown in the following example logs:

      2016-02-02 15:30:57.902  INFO [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
      +2016-02-02 15:30:58.372 ERROR [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
      +2016-02-02 15:31:01.936  INFO [bar,46ab0d418373cbc9,46ab0d418373cbc9,false] 23030 --- [nio-8081-exec-4] ...

      Notice the [appname,traceId,spanId,exportable] entries from the MDC:

      • spanId: The ID of a specific operation that took place.
      • appname: The name of the application that logged the span.
      • traceId: The ID of the latency graph that contains the span.
      • exportable: Whether the log should be exported to Zipkin. +When would you like the span not to be exportable? +When you want to wrap some operation in a Span and have it written to the logs only.
    • Provides an abstraction over common distributed tracing data models: traces, spans (forming a DAG), annotations, and key-value annotations. +Spring Cloud Sleuth is loosely based on HTrace but is compatible with Zipkin (Dapper).
    • Sleuth records timing information to aid in latency analysis. +By using sleuth, you can pinpoint causes of latency in your applications.
    • Sleuth is written to not log too much and to not cause your production application to crash. +To that end, Sleuth:

      • Propagates structural data about your call graph in-band and the rest out-of-band.
      • Includes opinionated instrumentation of layers such as HTTP.
      • Includes a sampling policy to manage volume.
      • Can report to a Zipkin system for query and visualization.
    • Instruments common ingress and egress points from Spring applications (servlet filter, async endpoints, rest template, scheduled actions, message channels, Zuul filters, and Feign client).
    • Sleuth includes default logic to join a trace across HTTP or messaging boundaries. +For example, HTTP propagation works over Zipkin-compatible request headers.
    • Sleuth can propagate context (also known as baggage) between processes. +Consequently, if you set a baggage element on a Span, it is sent downstream to other processes over either HTTP or messaging.
    • Provides a way to create or continue spans and add tags and logs through annotations.
    • If spring-cloud-sleuth-zipkin is on the classpath, the app generates and collects Zipkin-compatible traces. +By default, it sends them over HTTP to a Zipkin server on localhost (port 9411). +You can configure the location of the service by setting spring.zipkin.baseUrl.

      • If you depend on spring-rabbit, your app sends traces to a RabbitMQ broker instead of HTTP.
      • If you depend on spring-kafka, and set spring.zipkin.sender.type: kafka, your app sends traces to a Kafka broker instead of HTTP.
    [Caution]Caution

    spring-cloud-sleuth-stream is deprecated and should no longer be used.

    [Important]Important

    If you use Zipkin, configure the probability of spans exported by setting spring.sleuth.sampler.probability +(default: 0.1, which is 10 percent). Otherwise, you might think that Sleuth is not working be cause it omits some spans.

    [Note]Note

    The SLF4J MDC is always set and logback users immediately see the trace and span IDs in logs per the example +shown earlier. +Other logging systems have to configure their own formatter to get the same result. +The default is as follows: +logging.pattern.level set to %5p [${spring.zipkin.service.name:${spring.application.name:-}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}] +(this is a Spring Boot feature for logback users). +If you do not use SLF4J, this pattern is NOT automatically applied.

    52.1 Introduction to Brave

    [Important]Important

    Starting with version 2.0.0, Spring Cloud Sleuth uses +Brave as the tracing library. +For your convenience, we embed part of the Brave’s docs here.

    [Important]Important

    In the vast majority of cases you need to just use the Tracer +or SpanCustomizer beans from Brave that Sleuth provides. The documentation below contains +a high overview of what Brave is and how it works.

    Brave is a library used to capture and report latency information about distributed operations to Zipkin. +Most users do not use Brave directly. They use libraries or frameworks rather than employ Brave on their behalf.

    This module includes a tracer that creates and joins spans that model the latency of potentially distributed work. +It also includes libraries to propagate the trace context over network boundaries (for example, with HTTP headers).

    52.1.1 Tracing

    Most importantly, you need a brave.Tracer, configured to report to Zipkin.

    The following example setup sends trace data (spans) to Zipkin over HTTP (as opposed to Kafka):

    class MyClass {
    +
    +    private final Tracer tracer;
    +
    +    // Tracer will be autowired
    +    MyClass(Tracer tracer) {
    +        this.tracer = tracer;
    +    }
    +
    +    void doSth() {
    +        Span span = tracer.newTrace().name("encode").start();
    +        // ...
    +    }
    +}
    [Important]Important

    If your span contains a name longer than 50 chars, then that name is truncated to 50 chars. +Your names have to be explicit and concrete. +Big names lead to latency issues and sometimes even thrown exceptions.

    The tracer creates and joins spans that model the latency of potentially distributed work. +It can employ sampling to reduce overhead during the process, to reduce the amount of data sent to Zipkin, or both.

    Spans returned by a tracer report data to Zipkin when finished or do nothing if unsampled. +After starting a span, you can annotate events of interest or add tags containing details or lookup keys.

    Spans have a context that includes trace identifiers that place the span at the correct spot in the tree representing the distributed operation.

    52.1.2 Local Tracing

    When tracing code that never leaves your process, run it inside a scoped span.

    @Autowired Tracer tracer;
    +
    +// Start a new trace or a span within an existing trace representing an operation
    +ScopedSpan span = tracer.startScopedSpan("encode");
    +try {
    +  // The span is in "scope" meaning downstream code such as loggers can see trace IDs
    +  return encoder.encode();
    +} catch (RuntimeException | Error e) {
    +  span.error(e); // Unless you handle exceptions, you might not know the operation failed!
    +  throw e;
    +} finally {
    +  span.finish(); // always finish the span
    +}

    When you need more features, or finer control, use the Span type:

    @Autowired Tracer tracer;
    +
    +// Start a new trace or a span within an existing trace representing an operation
    +Span span = tracer.nextSpan().name("encode").start();
    +// Put the span in "scope" so that downstream code such as loggers can see trace IDs
    +try (SpanInScope ws = tracer.withSpanInScope(span)) {
    +  return encoder.encode();
    +} catch (RuntimeException | Error e) {
    +  span.error(e); // Unless you handle exceptions, you might not know the operation failed!
    +  throw e;
    +} finally {
    +  span.finish(); // note the scope is independent of the span. Always finish a span.
    +}

    Both of the above examples report the exact same span on finish!

    In the above example, the span will be either a new root span or the +next child in an existing trace.

    52.1.3 Customizing Spans

    Once you have a span, you can add tags to it. +The tags can be used as lookup keys or details. +For example, you might add a tag with your runtime version, as shown in the following example:

    span.tag("clnt/finagle.version", "6.36.0");

    When exposing the ability to customize spans to third parties, prefer brave.SpanCustomizer as opposed to brave.Span. +The former is simpler to understand and test and does not tempt users with span lifecycle hooks.

    interface MyTraceCallback {
    +  void request(Request request, SpanCustomizer customizer);
    +}

    Since brave.Span implements brave.SpanCustomizer, you can pass it to users, as shown in the following example:

    for (MyTraceCallback callback : userCallbacks) {
    +  callback.request(request, span);
    +}

    52.1.4 Implicitly Looking up the Current Span

    Sometimes, you do not know if a trace is in progress or not, and you do not want users to do null checks. +brave.CurrentSpanCustomizer handles this problem by adding data to any span that’s in progress or drops, as shown in the following example:

    Ex.

    // The user code can then inject this without a chance of it being null.
    +@Autowired SpanCustomizer span;
    +
    +void userCode() {
    +  span.annotate("tx.started");
    +  ...
    +}

    52.1.5 RPC tracing

    [Tip]Tip

    Check for instrumentation written here and Zipkin’s list before rolling your own RPC instrumentation.

    RPC tracing is often done automatically by interceptors. Behind the scenes, they add tags and events that relate to their role in an RPC operation.

    The following example shows how to add a client span:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// before you send a request, add metadata that describes the operation
    +span = tracer.nextSpan().name(service + "/" + method).kind(CLIENT);
    +span.tag("myrpc.version", "1.0.0");
    +span.remoteServiceName("backend");
    +span.remoteIpAndPort("172.3.4.1", 8108);
    +
    +// Add the trace context to the request, so it can be propagated in-band
    +tracing.propagation().injector(Request::addHeader)
    +                     .inject(span.context(), request);
    +
    +// when the request is scheduled, start the span
    +span.start();
    +
    +// if there is an error, tag the span
    +span.tag("error", error.getCode());
    +// or if there is an exception
    +span.error(exception);
    +
    +// when the response is complete, finish the span
    +span.finish();

    One-Way tracing

    Sometimes, you need to model an asynchronous operation where there is a +request but no response. In normal RPC tracing, you use span.finish() +to indicate that the response was received. In one-way tracing, you use +span.flush() instead, as you do not expect a response.

    The following example shows how a client might model a one-way operation:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// start a new span representing a client request
    +oneWaySend = tracer.nextSpan().name(service + "/" + method).kind(CLIENT);
    +
    +// Add the trace context to the request, so it can be propagated in-band
    +tracing.propagation().injector(Request::addHeader)
    +                     .inject(oneWaySend.context(), request);
    +
    +// fire off the request asynchronously, totally dropping any response
    +request.execute();
    +
    +// start the client side and flush instead of finish
    +oneWaySend.start().flush();

    The following example shows how a server might handle a one-way operation:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// pull the context out of the incoming request
    +extractor = tracing.propagation().extractor(Request::getHeader);
    +
    +// convert that context to a span which you can name and add tags to
    +oneWayReceive = nextSpan(tracer, extractor.extract(request))
    +    .name("process-request")
    +    .kind(SERVER)
    +    ... add tags etc.
    +
    +// start the server side and flush instead of finish
    +oneWayReceive.start().flush();
    +
    +// you should not modify this span anymore as it is complete. However,
    +// you can create children to represent follow-up work.
    +next = tracer.newSpan(oneWayReceive.context()).name("step2").start();
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__function_catalog_and_flexible_function_signatures.html b/Greenwich.SR5/multi/multi__function_catalog_and_flexible_function_signatures.html new file mode 100644 index 00000000..aac9a5a3 --- /dev/null +++ b/Greenwich.SR5/multi/multi__function_catalog_and_flexible_function_signatures.html @@ -0,0 +1,39 @@ + + + 129. Function Catalog and Flexible Function Signatures

    129. Function Catalog and Flexible Function Signatures

    One of the main features of Spring Cloud Function is to adapt and support a range of type signatures for user-defined functions, +while providing a consistent execution model. +That’s why all user defined functions are transformed into a canonical representation by FunctionCatalog, using primitives +defined by the Project Reactor (i.e., Flux<T> and Mono<T>). +Users can supply a bean of type Function<String,String>, for instance, and the FunctionCatalog will wrap it into a +Function<Flux<String>,Flux<String>>.

    Using Reactor based primitives not only helps with the canonical representation of user defined functions, but it also +facilitates a more robust and flexible(reactive) execution model.

    While users don’t normally have to care about the FunctionCatalog at all, it is useful to know what +kind of functions are supported in user code.

    129.1 Java 8 function support

    Generally speaking users can expect that if they write a function for +a plain old Java type (or primitive wrapper), then the function +catalog will wrap it to a Flux of the same type. If the user writes +a function using Message (from spring-messaging) it will receive and +transmit headers from any adapter that supports key-value metadata +(e.g. HTTP headers). Here are the details.

    User FunctionCatalog Registration 

    Function<S,T>

    Function<Flux<S>, Flux<T>>

     

    Function<Message<S>,Message<T>>

    Function<Flux<Message<S>>, Flux<Message<T>>>

     

    Function<Flux<S>, Flux<T>>

    Function<Flux<S>, Flux<T>> (pass through)

     

    Supplier<T>

    Supplier<Flux<T>>

     

    Supplier<Flux<T>>

    Supplier<Flux<T>>

     

    Consumer<T>

    Function<Flux<T>, Mono<Void>>

     

    Consumer<Message<T>>

    Function<Flux<Message<T>>, Mono<Void>>

     

    Consumer<Flux<T>>

    Consumer<Flux<T>>

     

    Consumer is a little bit special because it has a void return type, +which implies blocking, at least potentially. Most likely you will not +need to write Consumer<Flux<?>>, but if you do need to do that, +remember to subscribe to the input flux. If you declare a Consumer +of a non publisher type (which is normal), it will be converted to a +function that returns a publisher, so that it can be subscribed to in +a controlled way.

    129.2 Kotlin Lambda support

    We also provide support for Kotlin lambdas (since v2.0). +Consider the following:

    @Bean
    +open fun kotlinSupplier(): () -> String {
    +    return  { "Hello from Kotlin" }
    +}
    +
    +@Bean
    +open fun kotlinFunction(): (String) -> String {
    +    return  { it.toUpperCase() }
    +}
    +
    +@Bean
    +open fun kotlinConsumer(): (String) -> Unit {
    +    return  { println(it) }
    +}

    The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of +Supplier, Function and Consumer, and thus supported/recognized signatures by the framework. +While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the +same rules for signature transformation outlined in "Java 8 function support" section are applied here as well.

    To enable Kotlin support all you need is to add spring-cloud-function-kotlin module to your classpath which contains the appropriate +autoconfiguration and supporting classes.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__functional_bean_definitions.html b/Greenwich.SR5/multi/multi__functional_bean_definitions.html new file mode 100644 index 00000000..eac56a55 --- /dev/null +++ b/Greenwich.SR5/multi/multi__functional_bean_definitions.html @@ -0,0 +1,81 @@ + + + 133. Functional Bean Definitions

    133. Functional Bean Definitions

    Spring Cloud Function supports a "functional" style of bean declarations for small apps where you need fast startup. The functional style of bean declaration was a feature of Spring Framework 5.0 with significant enhancements in 5.1.

    133.1 Comparing Functional with Traditional Bean Definitions

    Here’s a vanilla Spring Cloud Function application from with the +familiar @Configuration and @Bean declaration style:

    @SpringBootApplication
    +public class DemoApplication {
    +
    +  @Bean
    +  public Function<String, String> uppercase() {
    +    return value -> value.toUpperCase();
    +  }
    +
    +  public static void main(String[] args) {
    +    SpringApplication.run(DemoApplication.class, args);
    +  }
    +
    +}

    You can run the above in a serverless platform, like AWS Lambda or Azure Functions, or you can run it in its own HTTP server just by including spring-cloud-function-starter-web on the classpath. Running the main method would expose an endpoint that you can use to ping that uppercase function:

    $ curl localhost:8080 -d foo
    +FOO

    The web adapter in spring-cloud-function-starter-web uses Spring MVC, so you needed a Servlet container. You can also use Webflux where the default server is netty (even though you can still use Servlet containers if you want to) - just include the spring-cloud-starter-function-webflux dependency instead. The functionality is the same, and the user application code can be used in both.

    Now for the functional beans: the user application code can be recast into "functional" +form, like this:

    @SpringBootConfiguration
    +public class DemoApplication implements ApplicationContextInitializer<GenericApplicationContext> {
    +
    +  public static void main(String[] args) {
    +    FunctionalSpringApplication.run(DemoApplication.class, args);
    +  }
    +
    +  public Function<String, String> uppercase() {
    +    return value -> value.toUpperCase();
    +  }
    +
    +  @Override
    +  public void initialize(GenericApplicationContext context) {
    +    context.registerBean("demo", FunctionRegistration.class,
    +        () -> new FunctionRegistration<>(uppercase())
    +            .type(FunctionType.from(String.class).to(String.class)));
    +  }
    +
    +}

    The main differences are:

    • The main class is an ApplicationContextInitializer.
    • The @Bean methods have been converted to calls to context.registerBean()
    • The @SpringBootApplication has been replaced with +@SpringBootConfiguration to signify that we are not enabling Spring +Boot autoconfiguration, and yet still marking the class as an "entry +point".
    • The SpringApplication from Spring Boot has been replaced with a +FunctionalSpringApplication from Spring Cloud Function (it’s a +subclass).

    The business logic beans that you register in a Spring Cloud Function app are of type FunctionRegistration. This is a wrapper that contains both the function and information about the input and output types. In the @Bean form of the application that information can be derived reflectively, but in a functional bean registration some of it is lost unless we use a FunctionRegistration.

    An alternative to using an ApplicationContextInitializer and FunctionRegistration is to make the application itself implement Function (or Consumer or Supplier). Example (equivalent to the above):

    @SpringBootConfiguration
    +public class DemoApplication implements Function<String, String> {
    +
    +  public static void main(String[] args) {
    +    FunctionalSpringApplication.run(DemoApplication.class, args);
    +  }
    +
    +  @Override
    +  public String uppercase(String value) {
    +    return value.toUpperCase();
    +  }
    +
    +}

    It would also work if you add a separate, standalone class of type Function and register it with the SpringApplication using an alternative form of the run() method. The main thing is that the generic type information is available at runtime through the class declaration.

    The app runs in its own HTTP server if you add spring-cloud-starter-function-webflux (it won’t work with the MVC starter at the moment because the functional form of the embedded Servlet container hasn’t been implemented). The app also runs just fine in AWS Lambda or Azure Functions, and the improvements in startup time are dramatic.

    [Note]Note

    The "lite" web server has some limitations for the range of Function signatures - in particular it doesn’t (yet) support Message input and output, but POJOs and any kind of Publisher should be fine.

    133.2 Testing Functional Applications

    Spring Cloud Function also has some utilities for integration testing that will be very familiar to Spring Boot users. For example, here is an integration test for the HTTP server wrapping the app above:

    @RunWith(SpringRunner.class)
    +@FunctionalSpringBootTest
    +@AutoConfigureWebTestClient
    +public class FunctionalTests {
    +
    +	@Autowired
    +	private WebTestClient client;
    +
    +	@Test
    +	public void words() throws Exception {
    +		client.post().uri("/").body(Mono.just("foo"), String.class).exchange()
    +				.expectStatus().isOk().expectBody(String.class).isEqualTo("FOO");
    +	}
    +
    +}

    This test is almost identical to the one you would write for the @Bean version of the same app - the only difference is the @FunctionalSpringBootTest annotation, instead of the regular @SpringBootTest. All the other pieces, like the @Autowired WebTestClient, are standard Spring Boot features.

    Or you could write a test for a non-HTTP app using just the FunctionCatalog. For example:

    @RunWith(SpringRunner.class)
    +@FunctionalSpringBootTest
    +public class FunctionalTests {
    +
    +	@Autowired
    +	private FunctionCatalog catalog;
    +
    +	@Test
    +	public void words() throws Exception {
    +		Function<Flux<String>, Flux<String>> function = catalog.lookup(Function.class,
    +				"function");
    +		assertThat(function.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO");
    +	}
    +
    +}

    (The FunctionCatalog always returns functions from Flux to Flux, even if the user declares them with a simpler signature.)

    133.3 Limitations of Functional Bean Declaration

    Most Spring Cloud Function apps have a relatively small scope compared to the whole of Spring Boot, so we are able to adapt it to these functional bean definitions easily. If you step outside that limited scope, you can extend your Spring Cloud Function app by switching back to @Bean style configuration, or by using a hybrid approach. If you want to take advantage of Spring Boot autoconfiguration for integrations with external datastores, for example, you will need to use @EnableAutoConfiguration. Your functions can still be defined using the functional declarations if you want (i.e. the "hybrid" style), but in that case you will need to explicitly switch off the "full functional mode" using spring.functional.enabled=false so that Spring Boot can take back control.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__gatewayfilter_factories.html b/Greenwich.SR5/multi/multi__gatewayfilter_factories.html new file mode 100644 index 00000000..ca0f1922 --- /dev/null +++ b/Greenwich.SR5/multi/multi__gatewayfilter_factories.html @@ -0,0 +1,434 @@ + + + 115. GatewayFilter Factories

    115. GatewayFilter Factories

    Route filters allow the modification of the incoming HTTP request or outgoing HTTP response in some manner. Route filters are scoped to a particular route. Spring Cloud Gateway includes many built-in GatewayFilter Factories.

    NOTE For more detailed examples on how to use any of the following filters, take a look at the unit tests.

    115.1 AddRequestHeader GatewayFilter Factory

    The AddRequestHeader GatewayFilter Factory takes a name and value parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_header_route
    +        uri: https://example.org
    +        filters:
    +        - AddRequestHeader=X-Request-Foo, Bar

    +

    This will add X-Request-Foo:Bar header to the downstream request’s headers for all matching requests.

    AddRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_header_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/{segment}
    +        filters:
    +        - AddRequestHeader=X-Request-Foo, Bar-{segment}

    +

    115.2 AddRequestParameter GatewayFilter Factory

    The AddRequestParameter GatewayFilter Factory takes a name and value parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_parameter_route
    +        uri: https://example.org
    +        filters:
    +        - AddRequestParameter=foo, bar

    +

    This will add foo=bar to the downstream request’s query string for all matching requests.

    AddRequestParameter is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_parameter_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - AddRequestParameter=foo, bar-{segment}

    +

    115.3 AddResponseHeader GatewayFilter Factory

    The AddResponseHeader GatewayFilter Factory takes a name and value parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_response_header_route
    +        uri: https://example.org
    +        filters:
    +        - AddResponseHeader=X-Response-Foo, Bar

    +

    This will add X-Response-Foo:Bar header to the downstream response’s headers for all matching requests.

    AddResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_response_header_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - AddResponseHeader=foo, bar-{segment}

    +

    115.4 DedupeResponseHeader GatewayFilter Factory

    The DedupeResponseHeader GatewayFilter Factory takes a name parameter and an optional strategy parameter. name can contain a list of header names, space separated.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: dedupe_response_header_route
    +        uri: https://example.org
    +        filters:
    +        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

    +

    This will remove duplicate values of Access-Control-Allow-Credentials and Access-Control-Allow-Origin response headers in cases when both the gateway CORS logic and the downstream add them.

    The DedupeResponseHeader filter also accepts an optional strategy parameter. The accepted values are RETAIN_FIRST (default), RETAIN_LAST, and RETAIN_UNIQUE.

    115.5 Hystrix GatewayFilter Factory

    Hystrix is a library from Netflix that implements the circuit breaker pattern. +The Hystrix GatewayFilter allows you to introduce circuit breakers to your gateway routes, protecting your services from cascading failures and allowing you to provide fallback responses in the event of downstream failures.

    To enable Hystrix GatewayFilters in your project, add a dependency on spring-cloud-starter-netflix-hystrix from Spring Cloud Netflix.

    The Hystrix GatewayFilter Factory requires a single name parameter, which is the name of the HystrixCommand.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: hystrix_route
    +        uri: https://example.org
    +        filters:
    +        - Hystrix=myCommandName

    +

    This wraps the remaining filters in a HystrixCommand with command name myCommandName.

    The Hystrix filter can also accept an optional fallbackUri parameter. Currently, only forward: schemed URIs are supported. If the fallback is called, the request will be forwarded to the controller matched by the URI.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: hystrix_route
    +        uri: lb://backing-service:8088
    +        predicates:
    +        - Path=/consumingserviceendpoint
    +        filters:
    +        - name: Hystrix
    +          args:
    +            name: fallbackcmd
    +            fallbackUri: forward:/incaseoffailureusethis
    +        - RewritePath=/consumingserviceendpoint, /backingserviceendpoint

    +

    This will forward to the /incaseoffailureusethis URI when the Hystrix fallback is called. Note that this example also demonstrates (optional) Spring Cloud Netflix Ribbon load-balancing via the lb prefix on the destination URI.

    The primary scenario is to use the fallbackUri to an internal controller or handler within the gateway app. +However, it is also possible to reroute the request to a controller or handler in an external application, like so:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: ingredients
    +        uri: lb://ingredients
    +        predicates:
    +        - Path=//ingredients/**
    +        filters:
    +        - name: Hystrix
    +          args:
    +            name: fetchIngredients
    +            fallbackUri: forward:/fallback
    +      - id: ingredients-fallback
    +        uri: http://localhost:9994
    +        predicates:
    +        - Path=/fallback

    +

    In this example, there is no fallback endpoint or handler in the gateway application, however, there is one in another +app, registered under http://localhost:9994.

    In case of the request being forwarded to fallback, the Hystrix Gateway filter also provides the Throwable that has +caused it. It’s added to the ServerWebExchange as the +ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR attribute that can be used when +handling the fallback within the gateway app.

    For the external controller/ handler scenario, headers can be added with exception details. You can find more information +on it in the FallbackHeaders GatewayFilter Factory section.

    Hystrix settings (such as timeouts) can be configured with global defaults or on a route by route basis using application properties as explained on the Hystrix wiki.

    To set a 5 second timeout for the example route above, the following configuration would be used:

    application.yml.  +

    hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000

    +

    115.6 FallbackHeaders GatewayFilter Factory

    The FallbackHeaders factory allows you to add Hystrix execution exception details in headers of a request forwarded to +a fallbackUri in an external application, like in the following scenario:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: ingredients
    +        uri: lb://ingredients
    +        predicates:
    +        - Path=//ingredients/**
    +        filters:
    +        - name: Hystrix
    +          args:
    +            name: fetchIngredients
    +            fallbackUri: forward:/fallback
    +      - id: ingredients-fallback
    +        uri: http://localhost:9994
    +        predicates:
    +        - Path=/fallback
    +        filters:
    +        - name: FallbackHeaders
    +          args:
    +            executionExceptionTypeHeaderName: Test-Header

    +

    In this example, after an execution exception occurs while running the HystrixCommand, the request will be forwarde to +the fallback endpoint or handler in an app running on localhost:9994. The headers with the exception type, message +and -if available- root cause exception type and message will be added to that request by the FallbackHeaders filter.

    The names of the headers can be overwritten in the config by setting the values of the arguments listed below, along with +their default values:

    • executionExceptionTypeHeaderName ("Execution-Exception-Type")
    • executionExceptionMessageHeaderName ("Execution-Exception-Message")
    • rootCauseExceptionTypeHeaderName ("Root-Cause-Exception-Type")
    • rootCauseExceptionMessageHeaderName ("Root-Cause-Exception-Message")

    You can find more information on how Hystrix works with Gateway in the Hystrix GatewayFilter Factory section.

    115.7 MapRequestHeader GatewayFilter Factory

    The MapRequestHeader GatewayFilter Facstory takes 'fromHeader' and 'toHeader' parameters. It creates a new named header (toHeader) and the value is extracted out of an existing named header (fromHeader) from the incoming http request. If the input header does not exist then the filter has no impact. If the new named header already exists then it’s values will be augmented with the new values.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: map_request_header_route
    +        uri: https://example.org
    +        filters:
    +        - MapRequestHeader=Bar, X-Request-Foo

    +

    This will add X-Request-Foo:<values> header to the downstream request’s with updated values from the incoming http request Bar header.

    115.8 PrefixPath GatewayFilter Factory

    The PrefixPath GatewayFilter Factory takes a single prefix parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: prefixpath_route
    +        uri: https://example.org
    +        filters:
    +        - PrefixPath=/mypath

    +

    This will prefix /mypath to the path of all matching requests. So a request to /hello, would be sent to /mypath/hello.

    115.9 PreserveHostHeader GatewayFilter Factory

    The PreserveHostHeader GatewayFilter Factory has no parameters. This filter, sets a request attribute that the routing filter will inspect to determine if the original host header should be sent, rather than the host header determined by the http client.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: preserve_host_route
    +        uri: https://example.org
    +        filters:
    +        - PreserveHostHeader

    +

    115.10 RequestRateLimiter GatewayFilter Factory

    The RequestRateLimiter GatewayFilter Factory is uses a RateLimiter implementation to determine if the current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by default) is returned.

    This filter takes an optional keyResolver parameter and parameters specific to the rate limiter (see below).

    keyResolver is a bean that implements the KeyResolver interface. In configuration, reference the bean by name using SpEL. #{@myKeyResolver} is a SpEL expression referencing a bean with the name myKeyResolver.

    KeyResolver.java.  +

    public interface KeyResolver {
    +	Mono<String> resolve(ServerWebExchange exchange);
    +}

    +

    The KeyResolver interface allows pluggable strategies to derive the key for limiting requests. In future milestones, there will be some KeyResolver implementations.

    The default implementation of KeyResolver is the PrincipalNameKeyResolver which retrieves the Principal from the ServerWebExchange and calls Principal.getName().

    By default, if the KeyResolver does not find a key, requests will be denied. This behavior can be adjust with the spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key (true or false) and spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code properties.

    [Note]Note

    The RequestRateLimiter is not configurable via the "shortcut" notation. The example below is invalid

    application.properties.  +

    # INVALID SHORTCUT CONFIGURATION
    +spring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2, 2, #{@userkeyresolver}

    +

    115.10.1 Redis RateLimiter

    The redis implementation is based off of work done at Stripe. It requires the use of the spring-boot-starter-data-redis-reactive Spring Boot starter.

    The algorithm used is the Token Bucket Algorithm.

    The redis-rate-limiter.replenishRate is how many requests per second do you want a user to be allowed to do, without any dropped requests. This is the rate that the token bucket is filled.

    The redis-rate-limiter.burstCapacity is the maximum number of requests a user is allowed to do in a single second. This is the number of tokens the token bucket can hold. Setting this value to zero will block all requests.

    A steady rate is accomplished by setting the same value in replenishRate and burstCapacity. Temporary bursts can be allowed by setting burstCapacity higher than replenishRate. In this case, the rate limiter needs to be allowed some time between bursts (according to replenishRate), as 2 consecutive bursts will result in dropped requests (HTTP 429 - Too Many Requests).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: requestratelimiter_route
    +        uri: https://example.org
    +        filters:
    +        - name: RequestRateLimiter
    +          args:
    +            redis-rate-limiter.replenishRate: 10
    +            redis-rate-limiter.burstCapacity: 20

    +

    Config.java.  +

    @Bean
    +KeyResolver userKeyResolver() {
    +    return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    +}

    +

    This defines a request rate limit of 10 per user. A burst of 20 is allowed, but the next second only 10 requests will be available. The KeyResolver is a simple one that gets the user request parameter (note: this is not recommended for production).

    A rate limiter can also be defined as a bean implementing the RateLimiter interface. In configuration, reference the bean by name using SpEL. #{@myRateLimiter} is a SpEL expression referencing a bean with the name myRateLimiter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: requestratelimiter_route
    +        uri: https://example.org
    +        filters:
    +        - name: RequestRateLimiter
    +          args:
    +            rate-limiter: "#{@myRateLimiter}"
    +            key-resolver: "#{@userKeyResolver}"

    +

    115.11 RedirectTo GatewayFilter Factory

    The RedirectTo GatewayFilter Factory takes a status and a url parameter. The status should be a 300 series redirect http code, such as 301. The url should be a valid url. This will be the value of the Location header.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: prefixpath_route
    +        uri: https://example.org
    +        filters:
    +        - RedirectTo=302, https://acme.org

    +

    This will send a status 302 with a Location:https://acme.org header to perform a redirect.

    115.12 RemoveRequestHeader GatewayFilter Factory

    The RemoveRequestHeader GatewayFilter Factory takes a name parameter. It is the name of the header to be removed.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: removerequestheader_route
    +        uri: https://example.org
    +        filters:
    +        - RemoveRequestHeader=X-Request-Foo

    +

    This will remove the X-Request-Foo header before it is sent downstream.

    115.13 RemoveResponseHeader GatewayFilter Factory

    The RemoveResponseHeader GatewayFilter Factory takes a name parameter. It is the name of the header to be removed.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: removeresponseheader_route
    +        uri: https://example.org
    +        filters:
    +        - RemoveResponseHeader=X-Response-Foo

    +

    This will remove the X-Response-Foo header from the response before it is returned to the gateway client.

    To remove any kind of sensitive header you should configure this filter for any routes that you may +want to do so. In addition you can configure this filter once using spring.cloud.gateway.default-filters +and have it applied to all routes.

    115.14 RewritePath GatewayFilter Factory

    The RewritePath GatewayFilter Factory takes a path regexp parameter and a replacement parameter. This uses Java regular expressions for a flexible way to rewrite the request path.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: rewritepath_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/**
    +        filters:
    +        - RewritePath=/foo(?<segment>/?.*), $\{segment}

    +

    For a request path of /foo/bar, this will set the path to /bar before making the downstream request. Notice the $\ which is replaced with $ because of the YAML spec.

    115.15 RewriteLocationResponseHeader GatewayFilter Factory

    The RewriteLocationResponseHeader GatewayFilter Factory modifies the value of Location response header, usually to get rid of backend specific details. It takes stripVersionMode, locationHeaderName, hostValue, and protocolsRegex parameters.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: rewritelocationresponseheader_route
    +        uri: http://example.org
    +        filters:
    +        - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,

    +

    For example, for a request POST https://api.example.com/some/object/name, Location response header value https://object-service.prod.example.net/v2/some/object/id will be rewritten as https://api.example.com/some/object/id.

    Parameter stripVersionMode has the following possible values: NEVER_STRIP, AS_IN_REQUEST (default), ALWAYS_STRIP.

    • NEVER_STRIP - Version will not be stripped, even if the original request path contains no version
    • AS_IN_REQUEST - Version will be stripped only if the original request path contains no version
    • ALWAYS_STRIP - Version will be stripped, even if the original request path contains version

    Parameter hostValue, if provided, will be used to replace the host:port portion of the response Location header. If not provided, the value of the Host request header will be used.

    Parameter protocolsRegex must be a valid regex String, against which the protocol name will be matched. If not matched, the filter will do nothing. Default is http|https|ftp|ftps.

    115.16 RewriteResponseHeader GatewayFilter Factory

    The RewriteResponseHeader GatewayFilter Factory takes name, regexp, and replacement parameters. It uses Java regular expressions for a flexible way to rewrite the response header value.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: rewriteresponseheader_route
    +        uri: https://example.org
    +        filters:
    +        - RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=***

    +

    For a header value of /42?user=ford&password=omg!what&flag=true, it will be set to /42?user=ford&password=***&flag=true after making the downstream request. Please use $\ to mean $ because of the YAML spec.

    115.17 SaveSession GatewayFilter Factory

    The SaveSession GatewayFilter Factory forces a WebSession::save operation before forwarding the call downstream. This is of particular use when +using something like Spring Session with a lazy data store and need to ensure the session state has been saved before making the forwarded call.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: save_session
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/**
    +        filters:
    +        - SaveSession

    +

    If you are integrating Spring Security with Spring Session, and want to ensure security details have been forwarded to the remote process, this is critical.

    115.18 SecureHeaders GatewayFilter Factory

    The SecureHeaders GatewayFilter Factory adds a number of headers to the response at the recommendation from this blog post.

    The following headers are added (along with default values):

    • X-Xss-Protection:1; mode=block
    • Strict-Transport-Security:max-age=631138519
    • X-Frame-Options:DENY
    • X-Content-Type-Options:nosniff
    • Referrer-Policy:no-referrer
    • Content-Security-Policy:default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'
    • X-Download-Options:noopen
    • X-Permitted-Cross-Domain-Policies:none

    To change the default values set the appropriate property in the spring.cloud.gateway.filter.secure-headers namespace:

    Property to change:

    • xss-protection-header
    • strict-transport-security
    • frame-options
    • content-type-options
    • referrer-policy
    • content-security-policy
    • download-options
    • permitted-cross-domain-policies

    To disable the default values set the property spring.cloud.gateway.filter.secure-headers.disable with comma separated values.

    [Note]Note

    Need use lowercase and full name of secure headers.

    The following values can use:

    • x-xss-protection
    • strict-transport-security
    • x-frame-options
    • x-content-type-options
    • referrer-policy
    • content-security-policy
    • x-download-options
    • x-permitted-cross-domain-policies

    Example: spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security

    115.19 SetPath GatewayFilter Factory

    The SetPath GatewayFilter Factory takes a path template parameter. It offers a simple way to manipulate the request path by allowing templated segments of the path. This uses the uri templates from Spring Framework. Multiple matching segments are allowed.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setpath_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/{segment}
    +        filters:
    +        - SetPath=/{segment}

    +

    For a request path of /foo/bar, this will set the path to /bar before making the downstream request.

    115.20 SetRequestHeader GatewayFilter Factory

    The SetRequestHeader GatewayFilter Factory takes name and value parameters.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setrequestheader_route
    +        uri: https://example.org
    +        filters:
    +        - SetRequestHeader=X-Request-Foo, Bar

    +

    This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a X-Request-Foo:1234, this would be replaced with X-Request-Foo:Bar, which is what the downstream service would receive.

    SetRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setrequestheader_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - SetRequestHeader=foo, bar-{segment}

    +

    115.21 SetResponseHeader GatewayFilter Factory

    The SetResponseHeader GatewayFilter Factory takes name and value parameters.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setresponseheader_route
    +        uri: https://example.org
    +        filters:
    +        - SetResponseHeader=X-Response-Foo, Bar

    +

    This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a X-Response-Foo:1234, this would be replaced with X-Response-Foo:Bar, which is what the gateway client would receive.

    SetResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setresponseheader_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - SetResponseHeader=foo, bar-{segment}

    +

    115.22 SetStatus GatewayFilter Factory

    The SetStatus GatewayFilter Factory takes a single status parameter. It must be a valid Spring HttpStatus. It may be the integer value 404 or the string representation of the enumeration NOT_FOUND.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setstatusstring_route
    +        uri: https://example.org
    +        filters:
    +        - SetStatus=BAD_REQUEST
    +      - id: setstatusint_route
    +        uri: https://example.org
    +        filters:
    +        - SetStatus=401

    +

    In either case, the HTTP status of the response will be set to 401.

    115.23 StripPrefix GatewayFilter Factory

    The StripPrefix GatewayFilter Factory takes one paramter, parts. The parts parameter indicated the number of parts in the path to strip from the request before sending it downstream.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: nameRoot
    +        uri: http://nameservice
    +        predicates:
    +        - Path=/name/**
    +        filters:
    +        - StripPrefix=2

    +

    When a request is made through the gateway to /name/bar/foo the request made to nameservice will look like http://nameservice/foo.

    115.24 Retry GatewayFilter Factory

    The Retry GatewayFilter Factory support following set of parameters:

    • retries: the number of retries that should be attempted
    • statuses: the HTTP status codes that should be retried, represented using org.springframework.http.HttpStatus
    • methods: the HTTP methods that should be retried, represented using org.springframework.http.HttpMethod
    • series: the series of status codes to be retried, represented using org.springframework.http.HttpStatus.Series
    • exceptions: list of exceptions thrown that should be retried
    • backoff: configured exponential backoff for the retries. Retries are performed after a backoff interval of firstBackoff * (factor ^ n) where n is the iteration. +If maxBackoff is configured, the maximum backoff applied will be limited to maxBackoff. +If basedOnPreviousValue is true, backoff will be calculated using prevBackoff * factor.

    The following defaults are configured for Retry filter if enabled:

    • retries — 3 times
    • series — 5XX series
    • methods — GET method
    • exceptions — IOException and TimeoutException
    • backoff — disabled

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: retry_test
    +        uri: http://localhost:8080/flakey
    +        predicates:
    +        - Host=*.retry.com
    +        filters:
    +        - name: Retry
    +          args:
    +            retries: 3
    +            statuses: BAD_GATEWAY
    +            methods: GET,POST
    +            backoff:
    +              firstBackoff: 10ms
    +              maxBackoff: 50ms
    +              factor: 2
    +              basedOnPreviousValue: false

    +

    [Note]Note

    When using the retry filter with a forward: prefixed URL, the target endpoint should be written carefully so that in case of an error it does not do anything that could result in a response being sent to the client and committed. For example, if the target endpoint is an annotated controller, the target controller method should not return ResponseEntity with an error status code. Instead it should throw an Exception, or signal an error, e.g. via a Mono.error(ex) return value, which the retry filter can be configured to handle by retrying.

    [Warning]Warning

    When using the retry filter with any HTTP method with a body, the body will be cached and the gateway will become memory constrained. The body is cached in a request attribute defined by ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR. The type of the object is a org.springframework.core.io.buffer.DataBuffer.

    115.25 RequestSize GatewayFilter Factory

    The RequestSize GatewayFilter Factory can restrict a request from reaching the downstream service , when the request size is greater than the permissible limit. The filter takes a maxSize parameter which is the permissible size limit of the request. The maxSize is a `DataSize type, so values can be defined as a number followed by an optional DataUnit suffix such as 'KB' or 'MB'. The default is 'B' for bytes.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: request_size_route
    +      uri: http://localhost:8080/upload
    +      predicates:
    +      - Path=/upload
    +      filters:
    +      - name: RequestSize
    +        args:
    +          maxSize: 5000000

    +

    The RequestSize GatewayFilter Factory set the response status as 413 Payload Too Large with a additional header errorMessage when the Request is rejected due to size. Following is an example of such an errorMessage .

    errorMessage : Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB

    [Note]Note

    The default Request size will be set to 5 MB if not provided as filter argument in route definition.

    115.26 Modify Request Body GatewayFilter Factory

    This filter is considered BETA and the API may change in the future

    The ModifyRequestBody filter can be used to modify the request body before it is sent downstream by the Gateway.

    [Note]Note

    This filter can only be configured using the Java DSL

    @Bean
    +public RouteLocator routes(RouteLocatorBuilder builder) {
    +    return builder.routes()
    +        .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
    +            .filters(f -> f.prefixPath("/httpbin")
    +                .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
    +                    (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
    +        .build();
    +}
    +
    +static class Hello {
    +    String message;
    +
    +    public Hello() { }
    +
    +    public Hello(String message) {
    +        this.message = message;
    +    }
    +
    +    public String getMessage() {
    +        return message;
    +    }
    +
    +    public void setMessage(String message) {
    +        this.message = message;
    +    }
    +}

    115.27 Modify Response Body GatewayFilter Factory

    This filter is considered BETA and the API may change in the future

    The ModifyResponseBody filter can be used to modify the response body before it is sent back to the Client.

    [Note]Note

    This filter can only be configured using the Java DSL

    @Bean
    +public RouteLocator routes(RouteLocatorBuilder builder) {
    +    return builder.routes()
    +        .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
    +            .filters(f -> f.prefixPath("/httpbin")
    +        		.modifyResponseBody(String.class, String.class,
    +        		    (exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri)
    +        .build();
    +}

    115.28 Default Filters

    If you would like to add a filter and apply it to all routes you can use spring.cloud.gateway.default-filters. +This property takes a list of filters

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      default-filters:
    +      - AddResponseHeader=X-Response-Default-Foo, Default-Bar
    +      - PrefixPath=/httpbin

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__getting_started.html b/Greenwich.SR5/multi/multi__getting_started.html new file mode 100644 index 00000000..dbbffd31 --- /dev/null +++ b/Greenwich.SR5/multi/multi__getting_started.html @@ -0,0 +1,9 @@ + + + 127. Getting Started

    127. Getting Started

    Build from the command line (and "install" the samples):

    $ ./mvnw clean install

    (If you like to YOLO add -DskipTests.)

    Run one of the samples, e.g.

    $ java -jar spring-cloud-function-samples/function-sample/target/*.jar

    This runs the app and exposes its functions over HTTP, so you can +convert a string to uppercase, like this:

    $ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d Hello
    +HELLO

    You can convert multiple strings (a Flux<String>) by separating them +with new lines

    $ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d 'Hello
    +> World'
    +HELLOWORLD

    (You can use QJ in a terminal to insert a new line in a literal +string like that.)

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__getting_started_2.html b/Greenwich.SR5/multi/multi__getting_started_2.html new file mode 100644 index 00000000..afc8d9a1 --- /dev/null +++ b/Greenwich.SR5/multi/multi__getting_started_2.html @@ -0,0 +1,5 @@ + + + 153. Getting started

    153. Getting started

    There are many available resources to get you up to speed with our libraries as quickly as possible.

    153.1 Spring Initializr

    There are three entries in Spring Initializr for Spring Cloud GCP.

    153.1.1 GCP Support

    The GCP Support entry contains auto-configuration support for every Spring Cloud GCP integration. +Most of the autoconfiguration code is only enabled if other dependencies are added to the classpath.

    Spring Cloud GCP StarterRequired dependencies

    Config

    org.springframework.cloud:spring-cloud-gcp-starter-config

    Cloud Spanner

    org.springframework.cloud:spring-cloud-gcp-starter-data-spanner

    Cloud Datastore

    org.springframework.cloud:spring-cloud-gcp-starter-data-datastore

    Logging

    org.springframework.cloud:spring-cloud-gcp-starter-logging

    SQL - MySql

    org.springframework.cloud:spring-cloud-gcp-starter-sql-mysql

    SQL - PostgreSQL

    org.springframework.cloud:spring-cloud-gcp-starter-sql-postgres

    Trace

    org.springframework.cloud:spring-cloud-gcp-starter-trace

    Vision

    org.springframework.cloud:spring-cloud-gcp-starter-vision

    Security - IAP

    org.springframework.cloud:spring-cloud-gcp-starter-security-iap

    153.1.2 GCP Messaging

    The GCP Messaging entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Pub/Sub integrations work out of the box.

    153.1.3 GCP Storage

    The GCP Storage entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Storage integrations work out of the box.

    153.2 Code Samples

    There are code samples available that demonstrate the usage of all our integrations.

    For example, the Vision API sample shows how to use spring-cloud-gcp-starter-vision to automatically configure Vision API clients.

    153.3 Code Challenges

    In a code challenge, you perform a task step by step, using one integration. +There are a number of challenges available in the Google Developers Codelabs page.

    153.4 Getting Started Guides

    A Spring Getting Started guide on messaging with Spring Integration Channel Adapters for Google Cloud Pub/Sub is available from Spring Guides.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__global_filters.html b/Greenwich.SR5/multi/multi__global_filters.html new file mode 100644 index 00000000..49407706 --- /dev/null +++ b/Greenwich.SR5/multi/multi__global_filters.html @@ -0,0 +1,79 @@ + + + 116. Global Filters

    116. Global Filters

    The GlobalFilter interface has the same signature as GatewayFilter. These are special filters that are conditionally applied to all routes. (This interface and usage are subject to change in future milestones).

    116.1 Combined Global Filter and GatewayFilter Ordering

    When a request comes in (and matches a Route) the Filtering Web Handler will add all instances of GlobalFilter and all route specific instances of GatewayFilter to a filter chain. This combined filter chain is sorted by the org.springframework.core.Ordered interface, which can be set by implementing the getOrder() method.

    As Spring Cloud Gateway distinguishes between "pre" and "post" phases for filter logic execution (see: How it Works), the filter with the highest precedence will be the first in the "pre"-phase and the last in the "post"-phase.

    ExampleConfiguration.java.  +

    @Bean
    +public GlobalFilter customFilter() {
    +    return new CustomGlobalFilter();
    +}
    +
    +public class CustomGlobalFilter implements GlobalFilter, Ordered {
    +
    +    @Override
    +    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    +        log.info("custom global filter");
    +        return chain.filter(exchange);
    +    }
    +
    +    @Override
    +    public int getOrder() {
    +        return -1;
    +    }
    +}

    +

    116.2 Forward Routing Filter

    The ForwardRoutingFilter looks for a URI in the exchange attribute ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a forward scheme (ie forward:///localendpoint), it will use the Spring DispatcherHandler to handler the request. The path part of the request URL will be overridden with the path in the forward URL. The unmodified original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute.

    116.3 LoadBalancerClient Filter

    The LoadBalancerClientFilter looks for a URI in the exchange attribute ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a lb scheme (ie lb://myservice), it will use the Spring Cloud LoadBalancerClient to resolve the name (myservice in the previous example) to an actual host and port and replace the URI in the same attribute. The unmodified original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute. The filter will also look in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR attribute to see if it equals lb and then the same rules apply.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: myRoute
    +        uri: lb://service
    +        predicates:
    +        - Path=/service/**

    +

    [Note]Note

    By default when a service instance cannot be found in the LoadBalancer a 503 will be returned. +You can configure the Gateway to return a 404 by setting spring.cloud.gateway.loadbalancer.use404=true.

    [Note]Note

    The isSecure value of the ServiceInstance returned from the LoadBalancer will override +the scheme specified in the request made to the Gateway. For example, if the request comes into the Gateway over HTTPS +but the ServiceInstance indicates it is not secure, then the downstream request will be made over +HTTP. The opposite situation can also apply. However if GATEWAY_SCHEME_PREFIX_ATTR is specified for the +route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the +route URL will override the ServiceInstance configuration.

    [Warning]Warning

    LoadBalancerClientFilter uses a blocking Ribbon LoadBalancerClient under the hood. +We suggest you use ReactiveLoadBalancerClientFilter instead. +You can switch to using it by adding org.springframework.cloud:spring-cloud-loadbalancer dependency to your project +and setting the value of the spring.cloud.loadbalancer.ribbon.enabled to false.

    116.4 ReactiveLoadBalancerClientFilter

    The ReactiveLoadBalancerClientFilter looks for a URI in the exchange attribute +ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a lb scheme (ie lb://myservice), +it will use the Spring Cloud ReactorLoadBalancer to resolve the name (myservice in the previous example) +to an actual host and port and replace the URI in the same attribute. The unmodified +original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute. +The filter will also look in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR attribute to see if it equals +lb and then the same rules apply.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: myRoute
    +        uri: lb://service
    +        predicates:
    +        - Path=/service/**

    +

    [Note]Note

    By default when a service instance cannot be found by the ReactorLoadBalancer, a 503 will be returned. +You can configure the Gateway to return a 404 by setting spring.cloud.gateway.loadbalancer.use404=true.

    [Note]Note

    The isSecure value of the ServiceInstance returned from the ReactiveLoadBalancerClientFilter will override +the scheme specified in the request made to the Gateway. For example, if the request comes into the Gateway over HTTPS +but the ServiceInstance indicates it is not secure, then the downstream request will be made over +HTTP. The opposite situation can also apply. However if GATEWAY_SCHEME_PREFIX_ATTR is specified for the +route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the +route URL will override the ServiceInstance configuration.

    116.5 Netty Routing Filter

    The Netty Routing Filter runs if the url located in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute has a http or https scheme. It uses the Netty HttpClient to make the downstream proxy request. The response is put in the ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR exchange attribute for use in a later filter. (There is an experimental WebClientHttpRoutingFilter that performs the same function, but does not require netty)

    116.6 Netty Write Response Filter

    The NettyWriteResponseFilter runs if there is a Netty HttpClientResponse in the ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR exchange attribute. It is run after all other filters have completed and writes the proxy response back to the gateway client response. (There is an experimental WebClientWriteResponseFilter that performs the same function, but does not require netty)

    116.7 RouteToRequestUrl Filter

    The RouteToRequestUrlFilter runs if there is a Route object in the ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR exchange attribute. It creates a new URI, based off of the request URI, but updated with the URI attribute of the Route object. The new URI is placed in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute.

    If the URI has a scheme prefix, such as lb:ws://serviceid, the lb scheme is stripped from the URI and placed in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR for use later in the filter chain.

    116.8 Websocket Routing Filter

    The Websocket Routing Filter runs if the url located in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute has a ws or wss scheme. It uses the Spring Web Socket infrastructure to forward the Websocket request downstream.

    Websockets may be load-balanced by prefixing the URI with lb, such as lb:ws://serviceid.

    [Note]Note

    If you are using SockJS as a fallback over normal http, you should configure a normal HTTP route as well as the Websocket Route.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      # SockJS route
    +      - id: websocket_sockjs_route
    +        uri: http://localhost:3001
    +        predicates:
    +        - Path=/websocket/info/**
    +      # Normal Websocket route
    +      - id: websocket_route
    +        uri: ws://localhost:3001
    +        predicates:
    +        - Path=/websocket/**

    +

    116.9 Gateway Metrics Filter

    To enable Gateway Metrics add spring-boot-starter-actuator as a project dependency. Then, by default, the Gateway Metrics Filter runs as long as the property spring.cloud.gateway.metrics.enabled is not set to false. This filter adds a timer metric named "gateway.requests" with the following tags:

    • routeId: The route id
    • routeUri: The URI that the API will be routed to
    • outcome: Outcome as classified by HttpStatus.Series
    • status: Http Status of the request returned to the client
    • httpStatusCode: Http Status of the request returned to the client
    • httpMethod: The Http method used for the request

    These metrics are then available to be scraped from /actuator/metrics/gateway.requests and can be easily integrated with Prometheus to create a Grafana dashboard.

    [Note]Note

    To enable the prometheus endpoint add micrometer-registry-prometheus as a project dependency.

    116.10 Marking An Exchange As Routed

    After the Gateway has routed a ServerWebExchange it will mark that exchange as "routed" by adding gatewayAlreadyRouted +to the exchange attributes. Once a request has been marked as routed, other routing filters will not route the request again, +essentially skipping the filter. There are convenience methods that you can use to mark an exchange as routed +or check if an exchange has already been routed.

    • ServerWebExchangeUtils.isAlreadyRouted takes a ServerWebExchange object and checks if it has been "routed"
    • ServerWebExchangeUtils.setAlreadyRouted takes a ServerWebExchange object and marks it as "routed"
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__glossary.html b/Greenwich.SR5/multi/multi__glossary.html new file mode 100644 index 00000000..c8586cb5 --- /dev/null +++ b/Greenwich.SR5/multi/multi__glossary.html @@ -0,0 +1,3 @@ + + + 111. Glossary

    111. Glossary

    • Route: Route the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true.
    • Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This allows developers to match on anything from the HTTP request, such as headers or parameters.
    • Filter: These are instances Spring Framework GatewayFilter constructed in with a specific factory. Here, requests and responses can be modified before or after sending the downstream request.
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__google_cloud_pubsub.html b/Greenwich.SR5/multi/multi__google_cloud_pubsub.html new file mode 100644 index 00000000..f1c69167 --- /dev/null +++ b/Greenwich.SR5/multi/multi__google_cloud_pubsub.html @@ -0,0 +1,76 @@ + + + 155. Google Cloud Pub/Sub

    155. Google Cloud Pub/Sub

    Spring Cloud GCP provides an abstraction layer to publish to and subscribe from Google Cloud Pub/Sub topics and to create, list or delete Google Cloud Pub/Sub topics and subscriptions.

    A Spring Boot starter is provided to auto-configure the various required Pub/Sub components.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-pubsub'
    +}

    This starter is also available from Spring Initializr through the GCP Messaging entry.

    155.1 Pub/Sub Operations & Template

    PubSubOperations is an abstraction that allows Spring users to use Google Cloud Pub/Sub without depending on any Google Cloud Pub/Sub API semantics. +It provides the common set of operations needed to interact with Google Cloud Pub/Sub. +PubSubTemplate is the default implementation of PubSubOperations and it uses the Google Cloud Java Client for Pub/Sub to interact with Google Cloud Pub/Sub.

    PubSubTemplate depends on a PublisherFactory and a SubscriberFactory. +The PublisherFactory provides a Google Cloud Java Client for Pub/Sub Publisher. +The SubscriberFactory provides the Subscriber for asynchronous message pulling, as well as a SubscriberStub for synchronous pulling. +The Spring Boot starter for GCP Pub/Sub auto-configures a PublisherFactory and SubscriberFactory with default settings and uses the GcpProjectIdProvider and CredentialsProvider auto-configured by the Spring Boot GCP starter.

    The PublisherFactory implementation provided by Spring Cloud GCP Pub/Sub, DefaultPublisherFactory, caches Publisher instances by topic name, in order to optimize resource utilization.

    The PubSubOperations interface is actually a combination of PubSubPublisherOperations and PubSubSubscriberOperations with the corresponding PubSubPublisherTemplate and PubSubSubscriberTemplate implementations, which can be used individually or via the composite PubSubTemplate. +The rest of the documentation refers to PubSubTemplate, but the same applies to PubSubPublisherTemplate and PubSubSubscriberTemplate, depending on whether we’re talking about publishing or subscribing.

    155.1.1 Publishing to a topic

    PubSubTemplate provides asynchronous methods to publish messages to a Google Cloud Pub/Sub topic. +The publish() method takes in a topic name to post the message to, a payload of a generic type and, optionally, a map with the message headers.

    Here is an example of how to publish a message to a Google Cloud Pub/Sub topic:

    public void publishMessage() {
    +    this.pubSubTemplate.publish("topic", "your message payload", ImmutableMap.of("key1", "val1"));
    +}

    By default, the SimplePubSubMessageConverter is used to convert payloads of type byte[], ByteString, ByteBuffer, and String to Pub/Sub messages.

    JSON support

    For serialization and deserialization of POJOs using Jackson JSON, configure a JacksonPubSubMessageConverter bean, and the Spring Boot starter for GCP Pub/Sub will automatically wire it into the PubSubTemplate.

    // Note: The ObjectMapper is used to convert Java POJOs to and from JSON.
    +// You will have to configure your own instance if you are unable to depend
    +// on the ObjectMapper provided by Spring Boot starters.
    +@Bean
    +public JacksonPubSubMessageConverter jacksonPubSubMessageConverter(ObjectMapper objectMapper) {
    +    return new JacksonPubSubMessageConverter(objectMapper);
    +}

    Alternatively, you can set it directly by calling the setMessageConverter() method on the PubSubTemplate. +Other implementations of the PubSubMessageConverter can also be configured in the same manner.

    Please refer to our Pub/Sub JSON Payload Sample App as a reference for using this functionality.

    155.1.2 Subscribing to a subscription

    Google Cloud Pub/Sub allows many subscriptions to be associated to the same topic. +PubSubTemplate allows you to listen to subscriptions via the subscribe() method. +It relies on a SubscriberFactory object, whose only task is to generate Google Cloud Pub/Sub +Subscriber objects. +When listening to a subscription, messages will be pulled from Google Cloud Pub/Sub +asynchronously, at a certain interval.

    The Spring Boot starter for Google Cloud Pub/Sub auto-configures a SubscriberFactory.

    If Pub/Sub message payload conversion is desired, you can use the subscribeAndConvert() method, which will use the converter configured in the template.

    155.1.3 Pulling messages from a subscription

    Google Cloud Pub/Sub supports synchronous pulling of messages from a subscription. +This is different from subscribing to a subscription, in the sense that subscribing is an asynchronous task which polls the subscription on a set interval.

    The pullNext() method allows for a single message to be pulled and automatically acknowledged from a subscription. +The pull() method pulls a number of messages from a subscription, allowing for the retry settings to be configured. +Any messages received by pull() are not automatically acknowledged. +Instead, since they are of the kind AcknowledgeablePubsubMessage, you can acknowledge them by calling the ack() method, or negatively acknowledge them by calling the nack() method. +The pullAndAck() method does the same as the pull() method and, additionally, acknowledges all received messages.

    The pullAndConvert() method does the same as the pull() method and, additionally, converts the Pub/Sub binary payload to an object of the desired type, using the converter configured in the template.

    To acknowledge multiple messages received from pull() or pullAndConvert() at once, you can use the PubSubTemplate.ack() method. +You can also use the PubSubTemplate.nack() for negatively acknowledging messages.

    Using these methods for acknowledging messages in batches is more efficient than acknowledging messages individually, but they require the collection of messages to be from the same project.

    All ack(), nack(), and modifyAckDeadline() methods on messages as well as PubSubSubscriberTemplate are implemented asynchronously, returning a ListenableFuture<Void> to be able to process the asynchronous execution.

    PubSubTemplate uses a special subscriber generated by its SubscriberFactory to synchronously pull messages.

    155.2 Pub/Sub management

    PubSubAdmin is the abstraction provided by Spring Cloud GCP to manage Google Cloud Pub/Sub resources. +It allows for the creation, deletion and listing of topics and subscriptions.

    PubSubAdmin depends on GcpProjectIdProvider and either a CredentialsProvider or a TopicAdminClient and a SubscriptionAdminClient. +If given a CredentialsProvider, it creates a TopicAdminClient and a SubscriptionAdminClient with the Google Cloud Java Library for Pub/Sub default settings. +The Spring Boot starter for GCP Pub/Sub auto-configures a PubSubAdmin object using the GcpProjectIdProvider and the CredentialsProvider auto-configured by the Spring Boot GCP Core starter.

    155.2.1 Creating a topic

    PubSubAdmin implements a method to create topics:

    public Topic createTopic(String topicName)

    Here is an example of how to create a Google Cloud Pub/Sub topic:

    public void newTopic() {
    +    pubSubAdmin.createTopic("topicName");
    +}

    155.2.2 Deleting a topic

    PubSubAdmin implements a method to delete topics:

    public void deleteTopic(String topicName)

    Here is an example of how to delete a Google Cloud Pub/Sub topic:

    public void deleteTopic() {
    +    pubSubAdmin.deleteTopic("topicName");
    +}

    155.2.3 Listing topics

    PubSubAdmin implements a method to list topics:

    public List<Topic> listTopics

    Here is an example of how to list every Google Cloud Pub/Sub topic name in a project:

    public List<String> listTopics() {
    +    return pubSubAdmin
    +        .listTopics()
    +        .stream()
    +        .map(Topic::getNameAsTopicName)
    +        .map(TopicName::getTopic)
    +        .collect(Collectors.toList());
    +}

    155.2.4 Creating a subscription

    PubSubAdmin implements a method to create subscriptions to existing topics:

    public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline, String pushEndpoint)

    Here is an example of how to create a Google Cloud Pub/Sub subscription:

    public void newSubscription() {
    +    pubSubAdmin.createSubscription("subscriptionName", "topicName", 10, “https://my.endpoint/push”);
    +}

    Alternative methods with default settings are provided for ease of use. +The default value for ackDeadline is 10 seconds. +If pushEndpoint isn’t specified, the subscription uses message pulling, instead.

    public Subscription createSubscription(String subscriptionName, String topicName)
    public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline)
    public Subscription createSubscription(String subscriptionName, String topicName, String pushEndpoint)

    155.2.5 Deleting a subscription

    PubSubAdmin implements a method to delete subscriptions:

    public void deleteSubscription(String subscriptionName)

    Here is an example of how to delete a Google Cloud Pub/Sub subscription:

    public void deleteSubscription() {
    +    pubSubAdmin.deleteSubscription("subscriptionName");
    +}

    155.2.6 Listing subscriptions

    PubSubAdmin implements a method to list subscriptions:

    public List<Subscription> listSubscriptions()

    Here is an example of how to list every subscription name in a project:

    public List<String> listSubscriptions() {
    +    return pubSubAdmin
    +        .listSubscriptions()
    +        .stream()
    +        .map(Subscription::getNameAsSubscriptionName)
    +        .map(SubscriptionName::getSubscription)
    +        .collect(Collectors.toList());
    +}

    155.3 Configuration

    The Spring Boot starter for Google Cloud Pub/Sub provides the following configuration options:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.pubsub.enabled

    Enables or disables Pub/Sub auto-configuration

    No

    true

    spring.cloud.gcp.pubsub.subscriber.executor-threads

    Number of threads used by Subscriber instances created by SubscriberFactory

    No

    4

    spring.cloud.gcp.pubsub.publisher.executor-threads

    Number of threads used by Publisher instances created by PublisherFactory

    No

    4

    spring.cloud.gcp.pubsub.project-id

    GCP project ID where the Google Cloud Pub/Sub API is hosted, if different from the one in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.pubsub.credentials.location

    OAuth2 credentials for authenticating with the +Google Cloud Pub/Sub API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.pubsub.credentials.encoded-key

    Base64-encoded contents of OAuth2 account private key for authenticating with the +Google Cloud Pub/Sub API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.pubsub.credentials.scopes

    OAuth2 scope for Spring Cloud GCP +Pub/Sub credentials

    No

    https://www.googleapis.com/auth/pubsub

    spring.cloud.gcp.pubsub.subscriber.parallel-pull-count

    The number of pull workers

    No

    The available number of processors

    spring.cloud.gcp.pubsub.subscriber.max-ack-extension-period

    The maximum period a message ack deadline will be extended, in seconds

    No

    0

    spring.cloud.gcp.pubsub.subscriber.pull-endpoint

    The endpoint for synchronous pulling messages

    No

    pubsub.googleapis.com:443

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.total-timeout-seconds

    TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. +The higher the total timeout, the more retries can be attempted.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-retry-delay-second

    InitialRetryDelay controls the delay before the first retry. +Subsequent retries will use this value adjusted according to the RetryDelayMultiplier.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.retry-delay-multiplier

    RetryDelayMultiplier controls the change in retry delay. +The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call.

    No

    1

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-retry-delay-seconds

    MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier +can’t increase the retry delay higher than this amount.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-attempts

    MaxAttempts defines the maximum number of attempts to perform. +If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.jittered

    Jitter determines if the delay time should be randomized.

    No

    true

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-rpc-timeout-seconds

    InitialRpcTimeout controls the timeout for the initial RPC. +Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.rpc-timeout-multiplier

    RpcTimeoutMultiplier controls the change in RPC timeout. +The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call.

    No

    1

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-rpc-timeout-seconds

    MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier +can’t increase the RPC timeout higher than this amount.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-element-count

    Maximum number of outstanding elements to keep in memory before enforcing flow control.

    No

    unlimited

    spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-request-bytes

    Maximum number of outstanding bytes to keep in memory before enforcing flow control.

    No

    unlimited

    spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.limit-exceeded-behavior

    The behavior when the specified limits are exceeded.

    No

    Block

    spring.cloud.gcp.pubsub.publisher.batching.element-count-threshold

    The element count threshold to use for batching.

    No

    unset (threshold does not apply)

    spring.cloud.gcp.pubsub.publisher.batching.request-byte-threshold

    The request byte threshold to use for batching.

    No

    unset (threshold does not apply)

    spring.cloud.gcp.pubsub.publisher.batching.delay-threshold-seconds

    The delay threshold to use for batching. +After this amount of time has elapsed (counting from the first element added), the elements will be wrapped up in a batch and sent.

    No

    unset (threshold does not apply)

    spring.cloud.gcp.pubsub.publisher.batching.enabled

    Enables batching.

    No

    false

    155.4 Sample

    A sample application is available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__google_cloud_vision.html b/Greenwich.SR5/multi/multi__google_cloud_vision.html new file mode 100644 index 00000000..4f1043f9 --- /dev/null +++ b/Greenwich.SR5/multi/multi__google_cloud_vision.html @@ -0,0 +1,27 @@ + + + 167. Google Cloud Vision

    167. Google Cloud Vision

    The Google Cloud Vision API allows users to leverage machine learning algorithms for processing images including: image classification, face detection, text extraction, and others.

    Spring Cloud GCP provides:

    • A convenience starter which automatically configures authentication settings and client objects needed to begin using the Google Cloud Vision API.
    • A Cloud Vision Template which simplifies interactions with the Cloud Vision API.

      • Allows you to easily send images to the API as Spring Resources.
      • Offers convenience methods for common operations, such as extracting the text from an image.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-gcp-starter-vision</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +  compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-vision'
    +}

    167.1 Cloud Vision Template

    The CloudVisionTemplate offers a simple way to use the Cloud Vision APIs with Spring Resources.

    After you add the spring-cloud-gcp-starter-vision dependency to your project, you may @Autowire an instance of CloudVisionTemplate to use in your code.

    The CloudVisionTemplate offers the following method for interfacing with Cloud Vision:

    public AnnotateImageResponse analyzeImage(Resource imageResource, Feature.Type…​ featureTypes)

    Parameters:

    • Resource imageResource refers to the Spring Resource of the image object you wish to analyze. +The Google Cloud Vision documentation provides a list of the image types that they support.
    • Feature.Type…​ featureTypes refers to a var-arg array of Cloud Vision Features to extract from the image. +A feature refers to a kind of image analysis one wishes to perform on an image, such as label detection, OCR recognition, facial detection, etc. +One may specify multiple features to analyze within one request. +A full list of Cloud Vision Features is provided in the Cloud Vision Feature docs.

    Returns:

    • AnnotateImageResponse contains the results of all the feature analyses that were specified in the request. +For each feature type that you provide in the request, AnnotateImageResponse provides a getter method to get the result of that feature analysis. +For example, if you analyzed an image using the LABEL_DETECTION feature, you would retrieve the results from the response using annotateImageResponse.getLabelAnnotationsList().

      AnnotateImageResponse is provided by the Google Cloud Vision libraries; please consult the RPC reference or Javadoc for more details. +Additionally, you may consult the Cloud Vision docs to familiarize yourself with the concepts and features of the API.

    167.2 Detect Image Labels Example

    Image labeling refers to producing labels that describe the contents of an image. +Below is a code sample of how this is done using the Cloud Vision Spring Template.

    @Autowired
    +private ResourceLoader resourceLoader;
    +
    +@Autowired
    +private CloudVisionTemplate cloudVisionTemplate;
    +
    +public void processImage() {
    +  Resource imageResource = this.resourceLoader.getResource("my_image.jpg");
    +  AnnotateImageResponse response = this.cloudVisionTemplate.analyzeImage(
    +      imageResource, Type.LABEL_DETECTION);
    +  System.out.println("Image Classification results: " + response.getLabelAnnotationsList());
    +}

    167.3 Sample

    A Sample Spring Boot Application is provided to show how to use the Cloud Vision starter and template.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__health_indicator_5.html b/Greenwich.SR5/multi/multi__health_indicator_5.html new file mode 100644 index 00000000..fceaf52e --- /dev/null +++ b/Greenwich.SR5/multi/multi__health_indicator_5.html @@ -0,0 +1,5 @@ + + + 36. Health Indicator

    36. Health Indicator

    Spring Cloud Stream provides a health indicator for binders. +It is registered under the name binders and can be enabled or disabled by setting the management.health.binders.enabled property.

    By default management.health.binders.enabled is set to false. +Setting management.health.binders.enabled to true enables the health indicator, allowing you to access the /health endpoint to retrieve the binder health indicators.

    Health indicators are binder-specific and certain binder implementations may not necessarily provide a health indicator.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__http_clients.html b/Greenwich.SR5/multi/multi__http_clients.html new file mode 100644 index 00000000..21e328ec --- /dev/null +++ b/Greenwich.SR5/multi/multi__http_clients.html @@ -0,0 +1,7 @@ + + + 21. HTTP Clients

    21. HTTP Clients

    Spring Cloud Netflix automatically creates the HTTP client used by Ribbon, Feign, and Zuul for you. +However, you can also provide your own HTTP clients customized as you need them to be. +To do so, you can create a bean of type ClosableHttpClient if you +are using the Apache Http Cient or OkHttpClient if you are using OK HTTP.

    [Note]Note

    When you create your own HTTP client, you are also responsible for implementing the correct connection management strategies for these clients. +Doing so improperly can result in resource management issues.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__httpheadersfilters.html b/Greenwich.SR5/multi/multi__httpheadersfilters.html new file mode 100644 index 00000000..658f4104 --- /dev/null +++ b/Greenwich.SR5/multi/multi__httpheadersfilters.html @@ -0,0 +1,3 @@ + + + 117. HttpHeadersFilters

    117. HttpHeadersFilters

    HttpHeadersFilters are applied to requests before sending them downstream, such as in the NettyRoutingFilter.

    117.1 Forwarded Headers Filter

    The Forwarded Headers Filter creates a Forwarded header to send to the downstream service. It adds the Host header, scheme and port of the current request to any existing Forwarded header.

    117.2 RemoveHopByHop Headers Filter

    The RemoveHopByHop Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the IETF.

    The default removed headers are:

    • Connection
    • Keep-Alive
    • Proxy-Authenticate
    • Proxy-Authorization
    • TE
    • Trailer
    • Transfer-Encoding
    • Upgrade

    To change this, set the spring.cloud.gateway.filter.remove-non-proxy-headers.headers property to the list of header names to remove.

    117.3 XForwarded Headers Filter

    The XForwarded Headers Filter creates various a X-Forwarded-* headers to send to the downstream service. It users the Host header, scheme, port and path of the current request to create the various headers.

    Creating of individual headers can be controlled by the following boolean properties (defaults to true):

    • spring.cloud.gateway.x-forwarded.for.enabled
    • spring.cloud.gateway.x-forwarded.host.enabled
    • spring.cloud.gateway.x-forwarded.port.enabled
    • spring.cloud.gateway.x-forwarded.proto.enabled
    • spring.cloud.gateway.x-forwarded.prefix.enabled

    Appending multiple headers can be controlled by the following boolean properties (defaults to true):

    • spring.cloud.gateway.x-forwarded.for.append
    • spring.cloud.gateway.x-forwarded.host.append
    • spring.cloud.gateway.x-forwarded.port.append
    • spring.cloud.gateway.x-forwarded.proto.append
    • spring.cloud.gateway.x-forwarded.prefix.append
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__hystrix_timeouts_and_ribbon_clients.html b/Greenwich.SR5/multi/multi__hystrix_timeouts_and_ribbon_clients.html new file mode 100644 index 00000000..dcaadc09 --- /dev/null +++ b/Greenwich.SR5/multi/multi__hystrix_timeouts_and_ribbon_clients.html @@ -0,0 +1,63 @@ + + + 15. Hystrix Timeouts And Ribbon Clients

    15. Hystrix Timeouts And Ribbon Clients

    When using Hystrix commands that wrap Ribbon clients you want to make sure your Hystrix timeout +is configured to be longer than the configured Ribbon timeout, including any potential +retries that might be made. For example, if your Ribbon connection timeout is one second and +the Ribbon client might retry the request three times, than your Hystrix timeout should +be slightly more than three seconds.

    15.1 How to Include the Hystrix Dashboard

    To include the Hystrix Dashboard in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-hystrix-dashboard. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    To run the Hystrix Dashboard, annotate your Spring Boot main class with @EnableHystrixDashboard. +Then visit /hystrix and point the dashboard to an individual instance’s /hystrix.stream endpoint in a Hystrix client application.

    [Note]Note

    When connecting to a /hystrix.stream endpoint that uses HTTPS, the certificate used by the server must be trusted by the JVM. +If the certificate is not trusted, you must import the certificate into the JVM in order for the Hystrix Dashboard to make a successful connection to the stream endpoint.

    15.2 Turbine

    Looking at an individual instance’s Hystrix data is not very useful in terms of the overall health of the system. Turbine is an application that aggregates all of the relevant /hystrix.stream endpoints into a combined /turbine.stream for use in the Hystrix Dashboard. +Individual instances are located through Eureka. +Running Turbine requires annotating your main class with the @EnableTurbine annotation (for example, by using spring-cloud-starter-netflix-turbine to set up the classpath). +All of the documented configuration properties from the Turbine 1 wiki apply. +The only difference is that the turbine.instanceUrlSuffix does not need the port prepended, as this is handled automatically unless turbine.instanceInsertPort=false.

    [Note]Note

    By default, Turbine looks for the /hystrix.stream endpoint on a registered instance by looking up its hostName and port entries in Eureka and then appending /hystrix.stream to it. +If the instance’s metadata contains management.port, it is used instead of the port value for the /hystrix.stream endpoint. +By default, the metadata entry called management.port is equal to the management.port configuration property. +It can be overridden though with following configuration:

    eureka:
    +  instance:
    +    metadata-map:
    +      management.port: ${management.port:8081}

    The turbine.appConfig configuration key is a list of Eureka serviceIds that turbine uses to lookup instances. +The turbine stream is then used in the Hystrix dashboard with a URL similar to the following:

    https://my.turbine.server:8080/turbine.stream?cluster=CLUSTERNAME

    The cluster parameter can be omitted if the name is default. +The cluster parameter must match an entry in turbine.aggregator.clusterConfig. +Values returned from Eureka are upper-case. Consequently, the following example works if there is an application called customers registered with Eureka:

    turbine:
    +  aggregator:
    +    clusterConfig: CUSTOMERS
    +  appConfig: customers

    If you need to customize which cluster names should be used by Turbine (because you do not want to store cluster names in +turbine.aggregator.clusterConfig configuration), provide a bean of type TurbineClustersProvider.

    The clusterName can be customized by a SPEL expression in turbine.clusterNameExpression with root as an instance of InstanceInfo. +The default value is appName, which means that the Eureka serviceId becomes the cluster key (that is, the InstanceInfo for customers has an appName of CUSTOMERS). +A different example is turbine.clusterNameExpression=aSGName, which gets the cluster name from the AWS ASG name. +The following listing shows another example:

    turbine:
    +  aggregator:
    +    clusterConfig: SYSTEM,USER
    +  appConfig: customers,stores,ui,admin
    +  clusterNameExpression: metadata['cluster']

    In the preceding example, the cluster name from four services is pulled from their metadata map and is expected to have values that include SYSTEM and USER.

    To use the default cluster for all apps, you need a string literal expression (with single quotes and escaped with double quotes if it is in YAML as well):

    turbine:
    +  appConfig: customers,stores
    +  clusterNameExpression: "'default'"

    Spring Cloud provides a spring-cloud-starter-netflix-turbine that has all the dependencies you need to get a Turbine server running. To add Turbine, create a Spring Boot application and annotate it with @EnableTurbine.

    [Note]Note

    By default, Spring Cloud lets Turbine use the host and port to allow multiple processes per host, per cluster. +If you want the native Netflix behavior built into Turbine to not allow multiple processes per host, per cluster (the key to the instance ID is the hostname), set turbine.combineHostPort=false.

    15.2.1 Clusters Endpoint

    In some situations it might be useful for other applications to know what custers have been configured +in Turbine. To support this you can use the /clusters endpoint which will return a JSON array of +all the configured clusters.

    GET /clusters.  +

    [
    +  {
    +    "name": "RACES",
    +    "link": "http://localhost:8383/turbine.stream?cluster=RACES"
    +  },
    +  {
    +    "name": "WEB",
    +    "link": "http://localhost:8383/turbine.stream?cluster=WEB"
    +  }
    +]

    +

    This endpoint can be disabled by setting turbine.endpoints.clusters.enabled to false.

    15.3 Turbine Stream

    In some environments (such as in a PaaS setting), the classic Turbine model of pulling metrics from all the distributed Hystrix commands does not work. +In that case, you might want to have your Hystrix commands push metrics to Turbine. Spring Cloud enables that with messaging. +To do so on the client, add a dependency to spring-cloud-netflix-hystrix-stream and the spring-cloud-starter-stream-* of your choice. +See the Spring Cloud Stream documentation for details on the brokers and how to configure the client credentials. It should work out of the box for a local broker.

    On the server side, create a Spring Boot application and annotate it with @EnableTurbineStream. +The Turbine Stream server requires the use of Spring Webflux, therefore spring-boot-starter-webflux needs to be included in your project. +By default spring-boot-starter-webflux is included when adding spring-cloud-starter-netflix-turbine-stream to your application.

    You can then point the Hystrix Dashboard to the Turbine Stream Server instead of individual Hystrix streams. +If Turbine Stream is running on port 8989 on myhost, then put http://myhost:8989 in the stream input field in the Hystrix Dashboard. +Circuits are prefixed by their respective serviceId, followed by a dot (.), and then the circuit name.

    Spring Cloud provides a spring-cloud-starter-netflix-turbine-stream that has all the dependencies you need to get a Turbine Stream server running. +You can then add the Stream binder of your choice — such as spring-cloud-starter-stream-rabbit.

    Turbine Stream server also supports the cluster parameter. +Unlike Turbine server, Turbine Stream uses eureka serviceIds as cluster names and these are not configurable.

    If Turbine Stream server is running on port 8989 on my.turbine.server and you have two eureka serviceIds customers and products in your environment, the following URLs will be available on your Turbine Stream server. default and empty cluster name will provide all metrics that Turbine Stream server receives.

    https://my.turbine.sever:8989/turbine.stream?cluster=customers
    +https://my.turbine.sever:8989/turbine.stream?cluster=products
    +https://my.turbine.sever:8989/turbine.stream?cluster=default
    +https://my.turbine.sever:8989/turbine.stream

    So, you can use eureka serviceIds as cluster names for your Turbine dashboard (or any compatible dashboard). +You don’t need to configure any properties like turbine.appConfig, turbine.clusterNameExpression and turbine.aggregator.clusterConfig for your Turbine Stream server.

    [Note]Note

    Turbine Stream server gathers all metrics from the configured input channel with Spring Cloud Stream. It means that it doesn’t gather Hystrix metrics actively from each instance. It just can provide metrics that were already gathered into the input channel by each instance.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__instrumentation.html b/Greenwich.SR5/multi/multi__instrumentation.html new file mode 100644 index 00000000..c0e82b04 --- /dev/null +++ b/Greenwich.SR5/multi/multi__instrumentation.html @@ -0,0 +1,6 @@ + + + 57. Instrumentation

    57. Instrumentation

    Spring Cloud Sleuth automatically instruments all your Spring applications, so you should not have to do anything to activate it. +The instrumentation is added by using a variety of technologies according to the stack that is available. For example, for a servlet web application, we use a Filter, and, for Spring Integration, we use ChannelInterceptors.

    You can customize the keys used in span tags. +To limit the volume of span data, an HTTP request is, by default, tagged only with a handful of metadata, such as the status code, the host, and the URL. +You can add request headers by configuring spring.sleuth.keys.http.headers (a list of header names).

    [Note]Note

    Tags are collected and exported only if there is a Sampler that allows it. By default, there is no such Sampler, to ensure that there is no danger of accidentally collecting too much data without configuring something).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__integrations.html b/Greenwich.SR5/multi/multi__integrations.html new file mode 100644 index 00000000..117f29ee --- /dev/null +++ b/Greenwich.SR5/multi/multi__integrations.html @@ -0,0 +1,183 @@ + + + 64. Integrations

    64. Integrations

    64.1 OpenTracing

    Spring Cloud Sleuth is compatible with OpenTracing. +If you have OpenTracing on the classpath, we automatically register the OpenTracing Tracer bean. +If you wish to disable this, set spring.sleuth.opentracing.enabled to false

    64.2 Runnable and Callable

    If you wrap your logic in Runnable or Callable, you can wrap those classes in their Sleuth representative, as shown in the following example for Runnable:

    Runnable runnable = new Runnable() {
    +	@Override
    +	public void run() {
    +		// do some work
    +	}
    +
    +	@Override
    +	public String toString() {
    +		return "spanNameFromToStringMethod";
    +	}
    +};
    +// Manual `TraceRunnable` creation with explicit "calculateTax" Span name
    +Runnable traceRunnable = new TraceRunnable(this.tracing, spanNamer, runnable,
    +		"calculateTax");
    +// Wrapping `Runnable` with `Tracing`. That way the current span will be available
    +// in the thread of `Runnable`
    +Runnable traceRunnableFromTracer = this.tracing.currentTraceContext()
    +		.wrap(runnable);

    The following example shows how to do so for Callable:

    Callable<String> callable = new Callable<String>() {
    +	@Override
    +	public String call() throws Exception {
    +		return someLogic();
    +	}
    +
    +	@Override
    +	public String toString() {
    +		return "spanNameFromToStringMethod";
    +	}
    +};
    +// Manual `TraceCallable` creation with explicit "calculateTax" Span name
    +Callable<String> traceCallable = new TraceCallable<>(this.tracing, spanNamer,
    +		callable, "calculateTax");
    +// Wrapping `Callable` with `Tracing`. That way the current span will be available
    +// in the thread of `Callable`
    +Callable<String> traceCallableFromTracer = this.tracing.currentTraceContext()
    +		.wrap(callable);

    That way, you ensure that a new span is created and closed for each execution.

    64.3 Hystrix

    64.3.1 Custom Concurrency Strategy

    We register a custom HystrixConcurrencyStrategy called TraceCallable that wraps all Callable instances in their Sleuth representative. +The strategy either starts or continues a span, depending on whether tracing was already going on before the Hystrix command was called. +To disable the custom Hystrix Concurrency Strategy, set the spring.sleuth.hystrix.strategy.enabled to false.

    64.3.2 Manual Command setting

    Assume that you have the following HystrixCommand:

    HystrixCommand<String> hystrixCommand = new HystrixCommand<String>(setter) {
    +	@Override
    +	protected String run() throws Exception {
    +		return someLogic();
    +	}
    +};

    To pass the tracing information, you have to wrap the same logic in the Sleuth version of the HystrixCommand, which is called +TraceCommand, as shown in the following example:

    TraceCommand<String> traceCommand = new TraceCommand<String>(tracer, setter) {
    +	@Override
    +	public String doRun() throws Exception {
    +		return someLogic();
    +	}
    +};

    64.4 RxJava

    We registering a custom RxJavaSchedulersHook that wraps all Action0 instances in their Sleuth representative, which is called TraceAction. +The hook either starts or continues a span, depending on whether tracing was already going on before the Action was scheduled. +To disable the custom RxJavaSchedulersHook, set the spring.sleuth.rxjava.schedulers.hook.enabled to false.

    You can define a list of regular expressions for thread names for which you do not want spans to be created. +To do so, provide a comma-separated list of regular expressions in the spring.sleuth.rxjava.schedulers.ignoredthreads property.

    [Important]Important

    The suggest approach to reactive programming and Sleuth is to use +the Reactor support.

    64.5 HTTP integration

    Features from this section can be disabled by setting the spring.sleuth.web.enabled property with value equal to false.

    64.5.1 HTTP Filter

    Through the TracingFilter, all sampled incoming requests result in creation of a Span. +That Span’s name is http: + the path to which the request was sent. +For example, if the request was sent to /this/that then the name will be http:/this/that. +You can configure which URIs you would like to skip by setting the spring.sleuth.web.skipPattern property. +If you have ManagementServerProperties on classpath, its value of contextPath gets appended to the provided skip pattern. +If you want to reuse the Sleuth’s default skip patterns and just append your own, pass those patterns by using the spring.sleuth.web.additionalSkipPattern.

    By default, all the spring boot actuator endpoints are automatically added to the skip pattern. +If you want to disable this behaviour set spring.sleuth.web.ignore-auto-configured-skip-patterns +to true.

    To change the order of tracing filter registration, please set the +spring.sleuth.web.filter-order property.

    To disable the filter that logs uncaught exceptions you can disable the +spring.sleuth.web.exception-throwing-filter-enabled property.

    64.5.2 HandlerInterceptor

    Since we want the span names to be precise, we use a TraceHandlerInterceptor that either wraps an existing HandlerInterceptor or is added directly to the list of existing HandlerInterceptors. +The TraceHandlerInterceptor adds a special request attribute to the given HttpServletRequest. +If the the TracingFilter does not see this attribute, it creates a fallback span, which is an additional span created on the server side so that the trace is presented properly in the UI. +If that happens, there is probably missing instrumentation. +In that case, please file an issue in Spring Cloud Sleuth.

    64.5.3 Async Servlet support

    If your controller returns a Callable or a WebAsyncTask, Spring Cloud Sleuth continues the existing span instead of creating a new one.

    64.5.4 WebFlux support

    Through TraceWebFilter, all sampled incoming requests result in creation of a Span. +That Span’s name is http: + the path to which the request was sent. +For example, if the request was sent to /this/that, the name is http:/this/that. +You can configure which URIs you would like to skip by using the spring.sleuth.web.skipPattern property. +If you have ManagementServerProperties on the classpath, its value of contextPath gets appended to the provided skip pattern. +If you want to reuse Sleuth’s default skip patterns and append your own, pass those patterns by using the spring.sleuth.web.additionalSkipPattern.

    To change the order of tracing filter registration, please set the +spring.sleuth.web.filter-order property.

    64.5.5 Dubbo RPC support

    Via the integration with Brave, Spring Cloud Sleuth supports Dubbo. +It’s enough to add the brave-instrumentation-dubbo dependency:

    <dependency>
    +    <groupId>io.zipkin.brave</groupId>
    +    <artifactId>brave-instrumentation-dubbo</artifactId>
    +</dependency>

    You need to also set a dubbo.properties file with the following contents:

    dubbo.provider.filter=tracing
    +dubbo.consumer.filter=tracing

    You can read more about Brave - Dubbo integration here. +An example of Spring Cloud Sleuth and Dubbo can be found here.

    64.6 HTTP Client Integration

    64.6.1 Synchronous Rest Template

    We inject a RestTemplate interceptor to ensure that all the tracing information is passed to the requests. +Each time a call is made, a new Span is created. +It gets closed upon receiving the response. +To block the synchronous RestTemplate features, set spring.sleuth.web.client.enabled to false.

    [Important]Important

    You have to register RestTemplate as a bean so that the interceptors get injected. +If you create a RestTemplate instance with a new keyword, the instrumentation does NOT work.

    64.6.2 Asynchronous Rest Template

    [Important]Important

    Starting with Sleuth 2.0.0, we no longer register a bean of AsyncRestTemplate type. +It is up to you to create such a bean. +Then we instrument it.

    To block the AsyncRestTemplate features, set spring.sleuth.web.async.client.enabled to false. +To disable creation of the default TraceAsyncClientHttpRequestFactoryWrapper, set spring.sleuth.web.async.client.factory.enabled +to false. +If you do not want to create AsyncRestClient at all, set spring.sleuth.web.async.client.template.enabled to false.

    Multiple Asynchronous Rest Templates

    Sometimes you need to use multiple implementations of the Asynchronous Rest Template. +In the following snippet, you can see an example of how to set up such a custom AsyncRestTemplate:

    @Configuration
    +@EnableAutoConfiguration
    +static class Config {
    +
    +	@Bean(name = "customAsyncRestTemplate")
    +	public AsyncRestTemplate traceAsyncRestTemplate() {
    +		return new AsyncRestTemplate(asyncClientFactory(),
    +				clientHttpRequestFactory());
    +	}
    +
    +	private ClientHttpRequestFactory clientHttpRequestFactory() {
    +		ClientHttpRequestFactory clientHttpRequestFactory = new CustomClientHttpRequestFactory();
    +		// CUSTOMIZE HERE
    +		return clientHttpRequestFactory;
    +	}
    +
    +	private AsyncClientHttpRequestFactory asyncClientFactory() {
    +		AsyncClientHttpRequestFactory factory = new CustomAsyncClientHttpRequestFactory();
    +		// CUSTOMIZE HERE
    +		return factory;
    +	}
    +
    +}

    64.6.3 WebClient

    We inject a ExchangeFilterFunction implementation that creates a span and, through on-success and on-error callbacks, takes care of closing client-side spans.

    To block this feature, set spring.sleuth.web.client.enabled to false.

    [Important]Important

    You have to register WebClient as a bean so that the tracing instrumentation gets applied. +If you create a WebClient instance with a new keyword, the instrumentation does NOT work.

    64.6.4 Traverson

    If you use the Traverson library, you can inject a RestTemplate as a bean into your Traverson object. +Since RestTemplate is already intercepted, you get full support for tracing in your client. The following pseudo code +shows how to do that:

    @Autowired RestTemplate restTemplate;
    +
    +Traverson traverson = new Traverson(URI.create("http://some/address"),
    +    MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8).setRestOperations(restTemplate);
    +// use Traverson

    64.6.5 Apache HttpClientBuilder and HttpAsyncClientBuilder

    We instrument the HttpClientBuilder and HttpAsyncClientBuilder so that +tracing context gets injected to the sent requests.

    To block these features, set spring.sleuth.web.client.enabled to false.

    64.6.6 Netty HttpClient

    We instrument the Netty’s HttpClient.

    To block this feature, set spring.sleuth.web.client.enabled to false.

    [Important]Important

    You have to register HttpClient as a bean so that the instrumentation happens. +If you create a HttpClient instance with a new keyword, the instrumentation does NOT work.

    64.6.7 UserInfoRestTemplateCustomizer

    We instrument the Spring Security’s UserInfoRestTemplateCustomizer.

    To block this feature, set spring.sleuth.web.client.enabled to false.

    64.7 Feign

    By default, Spring Cloud Sleuth provides integration with Feign through TraceFeignClientAutoConfiguration. +You can disable it entirely by setting spring.sleuth.feign.enabled to false. +If you do so, no Feign-related instrumentation take place.

    Part of Feign instrumentation is done through a FeignBeanPostProcessor. +You can disable it by setting spring.sleuth.feign.processor.enabled to false. +If you set it to false, Spring Cloud Sleuth does not instrument any of your custom Feign components. +However, all the default instrumentation is still there.

    64.8 gRPC

    Spring Cloud Sleuth provides instrumentation for gRPC through TraceGrpcAutoConfiguration. You can disable it entirely by setting spring.sleuth.grpc.enabled to false.

    64.8.1 Variant 1

    Dependencies

    [Important]Important

    The gRPC integration relies on two external libraries to instrument clients and servers and both of those libraries must be on the class path to enable the instrumentation.

    Maven:

    		<dependency>
    +			<groupId>io.github.lognet</groupId>
    +			<artifactId>grpc-spring-boot-starter</artifactId>
    +		</dependency>
    +		<dependency>
    +			<groupId>io.zipkin.brave</groupId>
    +			<artifactId>brave-instrumentation-grpc</artifactId>
    +		</dependency>

    Gradle:

        compile("io.github.lognet:grpc-spring-boot-starter")
    +    compile("io.zipkin.brave:brave-instrumentation-grpc")

    Server Instrumentation

    Spring Cloud Sleuth leverages grpc-spring-boot-starter to register Brave’s gRPC server interceptor with all services annotated with @GRpcService.

    Client Instrumentation

    gRPC clients leverage a ManagedChannelBuilder to construct a ManagedChannel used to communicate to the gRPC server. The native ManagedChannelBuilder provides static methods as entry points for construction of ManagedChannel instances, however, this mechanism is outside the influence of the Spring application context.

    [Important]Important

    Spring Cloud Sleuth provides a SpringAwareManagedChannelBuilder that can be customized through the Spring application context and injected by gRPC clients. This builder must be used when creating ManagedChannel instances.

    Sleuth creates a TracingManagedChannelBuilderCustomizer which inject Brave’s client interceptor into the SpringAwareManagedChannelBuilder.

    64.8.2 Variant 2

    Grpc Spring Boot Starter automatically detects the presence of Spring Cloud Sleuth and brave’s instrumentation for gRPC and registers the necessary client and/or server tooling.

    64.9 Asynchronous Communication

    64.9.1 @Async Annotated methods

    In Spring Cloud Sleuth, we instrument async-related components so that the tracing information is passed between threads. +You can disable this behavior by setting the value of spring.sleuth.async.enabled to false.

    If you annotate your method with @Async, we automatically create a new Span with the following characteristics:

    • If the method is annotated with @SpanName, the value of the annotation is the Span’s name.
    • If the method is not annotated with @SpanName, the Span name is the annotated method name.
    • The span is tagged with the method’s class name and method name.

    64.9.2 @Scheduled Annotated Methods

    In Spring Cloud Sleuth, we instrument scheduled method execution so that the tracing information is passed between threads. +You can disable this behavior by setting the value of spring.sleuth.scheduled.enabled to false.

    If you annotate your method with @Scheduled, we automatically create a new span with the following characteristics:

    • The span name is the annotated method name.
    • The span is tagged with the method’s class name and method name.

    If you want to skip span creation for some @Scheduled annotated classes, you can set the spring.sleuth.scheduled.skipPattern with a regular expression that matches the fully qualified name of the @Scheduled annotated class. +If you use spring-cloud-sleuth-stream and spring-cloud-netflix-hystrix-stream together, a span is created for each Hystrix metrics and sent to Zipkin. +This behavior may be annoying. That’s why, by default, spring.sleuth.scheduled.skipPattern=org.springframework.cloud.netflix.hystrix.stream.HystrixStreamTask.

    64.9.3 Executor, ExecutorService, and ScheduledExecutorService

    We provide LazyTraceExecutor, TraceableExecutorService, and TraceableScheduledExecutorService. Those implementations create spans each time a new task is submitted, invoked, or scheduled.

    The following example shows how to pass tracing information with TraceableExecutorService when working with CompletableFuture:

    CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> {
    +	// perform some logic
    +	return 1_000_000L;
    +}, new TraceableExecutorService(beanFactory, executorService,
    +		// 'calculateTax' explicitly names the span - this param is optional
    +		"calculateTax"));
    [Important]Important

    Sleuth does not work with parallelStream() out of the box. +If you want to have the tracing information propagated through the stream, you have to use the approach with supplyAsync(…​), as shown earlier.

    If there are beans that implement the Executor interface that you would like +to exclude from span creation, you can use the spring.sleuth.async.ignored-beans +property where you can provide a list of bean names.

    Customization of Executors

    Sometimes, you need to set up a custom instance of the AsyncExecutor. +The following example shows how to set up such a custom Executor:

    @Configuration
    +@EnableAutoConfiguration
    +@EnableAsync
    +// add the infrastructure role to ensure that the bean gets auto-proxied
    +@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    +static class CustomExecutorConfig extends AsyncConfigurerSupport {
    +
    +	@Autowired
    +	BeanFactory beanFactory;
    +
    +	@Override
    +	public Executor getAsyncExecutor() {
    +		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    +		// CUSTOMIZE HERE
    +		executor.setCorePoolSize(7);
    +		executor.setMaxPoolSize(42);
    +		executor.setQueueCapacity(11);
    +		executor.setThreadNamePrefix("MyExecutor-");
    +		// DON'T FORGET TO INITIALIZE
    +		executor.initialize();
    +		return new LazyTraceExecutor(this.beanFactory, executor);
    +	}
    +
    +}
    [Tip]Tip

    To ensure that your configuration gets post processed, remember +to add the @Role(BeanDefinition.ROLE_INFRASTRUCTURE) on your +@Configuration class

    64.10 Messaging

    Features from this section can be disabled by setting the spring.sleuth.messaging.enabled property with value equal to false.

    64.10.1 Spring Integration and Spring Cloud Stream

    Spring Cloud Sleuth integrates with Spring Integration. +It creates spans for publish and subscribe events. +To disable Spring Integration instrumentation, set spring.sleuth.integration.enabled to false.

    You can provide the spring.sleuth.integration.patterns pattern to explicitly provide the names of channels that you want to include for tracing. +By default, all channels but hystrixStreamOutput channel are included.

    [Important]Important

    When using the Executor to build a Spring Integration IntegrationFlow, you must use the untraced version of the Executor. +Decorating the Spring Integration Executor Channel with TraceableExecutorService causes the spans to be improperly closed.

    If you want to customize the way tracing context is read from and written to message headers, +it’s enough for you to register beans of types:

    • Propagation.Setter<MessageHeaderAccessor, String> - for writing headers to the message
    • Propagation.Getter<MessageHeaderAccessor, String> - for reading headers from the message

    64.10.2 Spring RabbitMq

    We instrument the RabbitTemplate so that tracing headers get injected +into the message.

    To block this feature, set spring.sleuth.messaging.rabbit.enabled to false.

    64.10.3 Spring Kafka

    We instrument the Spring Kafka’s ProducerFactory and ConsumerFactory +so that tracing headers get injected into the created Spring Kafka’s +Producer and Consumer.

    To block this feature, set spring.sleuth.messaging.kafka.enabled to false.

    64.10.4 Spring JMS

    We instrument the JmsTemplate so that tracing headers get injected +into the message. We also support @JmsListener annotated methods on the consumer side.

    To block this feature, set spring.sleuth.messaging.jms.enabled to false.

    [Important]Important

    We don’t support baggage propagation for JMS

    64.11 Zuul

    We instrument the Zuul Ribbon integration by enriching the Ribbon requests with tracing information. +To disable Zuul support, set the spring.sleuth.zuul.enabled property to false.

    64.12 Project Reactor

    For projects depending on Project Reactor such as Spring Cloud Gateway, we suggest turning the spring.sleuth.reactor.decorate-on-each option to false. That way an increased performance gain should be observed in comparison to the standard instrumentation mechanism. What this option does is it will wrap decorate onLast operator instead of onEach which will result in creation of far fewer objects. The downside of this is that when Project Reactor will change threads, the trace propagation will continue without issues, however anything relying on the ThreadLocal such as e.g. MDC entries can be buggy.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__inter_application_communication.html b/Greenwich.SR5/multi/multi__inter_application_communication.html new file mode 100644 index 00000000..3ddc377b --- /dev/null +++ b/Greenwich.SR5/multi/multi__inter_application_communication.html @@ -0,0 +1,37 @@ + + + 34. Inter-Application Communication

    34. Inter-Application Communication

    Spring Cloud Stream enables communication between applications. Inter-application communication is a complex issue spanning several concerns, as described in the following topics:

    34.1 Connecting Multiple Application Instances

    While Spring Cloud Stream makes it easy for individual Spring Boot applications to connect to messaging systems, the typical scenario for Spring Cloud Stream is the creation of multi-application pipelines, where microservice applications send data to each other. +You can achieve this scenario by correlating the input and output destinations of adjacent applications.

    Suppose a design calls for the Time Source application to send data to the Log Sink application. You could use a common destination named ticktock for bindings within both applications.

    Time Source (that has the channel name output) would set the following property:

    spring.cloud.stream.bindings.output.destination=ticktock

    Log Sink (that has the channel name input) would set the following property:

    spring.cloud.stream.bindings.input.destination=ticktock

    34.2 Instance Index and Instance Count

    When scaling up Spring Cloud Stream applications, each instance can receive information about how many other instances of the same application exist and what its own instance index is. +Spring Cloud Stream does this through the spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex properties. +For example, if there are three instances of a HDFS sink application, all three instances have spring.cloud.stream.instanceCount set to 3, and the individual applications have spring.cloud.stream.instanceIndex set to 0, 1, and 2, respectively.

    When Spring Cloud Stream applications are deployed through Spring Cloud Data Flow, these properties are configured automatically; when Spring Cloud Stream applications are launched independently, these properties must be set correctly. +By default, spring.cloud.stream.instanceCount is 1, and spring.cloud.stream.instanceIndex is 0.

    In a scaled-up scenario, correct configuration of these two properties is important for addressing partitioning behavior (see below) in general, and the two properties are always required by certain binders (for example, the Kafka binder) in order to ensure that data are split correctly across multiple consumer instances.

    34.3 Partitioning

    Partitioning in Spring Cloud Stream consists of two tasks:

    34.3.1 Configuring Output Bindings for Partitioning

    You can configure an output binding to send partitioned data by setting one and only one of its partitionKeyExpression or partitionKeyExtractorName properties, as well as its partitionCount property.

    For example, the following is a valid and typical configuration:

    spring.cloud.stream.bindings.output.producer.partitionKeyExpression=payload.id
    +spring.cloud.stream.bindings.output.producer.partitionCount=5

    Based on that example configuration, data is sent to the target partition by using the following logic.

    A partition key’s value is calculated for each message sent to a partitioned output channel based on the partitionKeyExpression. +The partitionKeyExpression is a SpEL expression that is evaluated against the outbound message for extracting the partitioning key.

    If a SpEL expression is not sufficient for your needs, you can instead calculate the partition key value by providing an implementation of org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy and configuring it as a bean (by using the @Bean annotation). +If you have more then one bean of type org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy available in the Application Context, you can further filter it by specifying its name with the partitionKeyExtractorName property, as shown in the following example:

    --spring.cloud.stream.bindings.output.producer.partitionKeyExtractorName=customPartitionKeyExtractor
    +--spring.cloud.stream.bindings.output.producer.partitionCount=5
    +. . .
    +@Bean
    +public CustomPartitionKeyExtractorClass customPartitionKeyExtractor() {
    +    return new CustomPartitionKeyExtractorClass();
    +}
    [Note]Note

    In previous versions of Spring Cloud Stream, you could specify the implementation of org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy by setting the spring.cloud.stream.bindings.output.producer.partitionKeyExtractorClass property. +Since version 2.0, this property is deprecated, and support for it will be removed in a future version.

    Once the message key is calculated, the partition selection process determines the target partition as a value between 0 and partitionCount - 1. +The default calculation, applicable in most scenarios, is based on the following formula: key.hashCode() % partitionCount. +This can be customized on the binding, either by setting a SpEL expression to be evaluated against the 'key' (through the partitionSelectorExpression property) or by configuring an implementation of org.springframework.cloud.stream.binder.PartitionSelectorStrategy as a bean (by using the @Bean annotation). +Similar to the PartitionKeyExtractorStrategy, you can further filter it by using the spring.cloud.stream.bindings.output.producer.partitionSelectorName property when more than one bean of this type is available in the Application Context, as shown in the following example:

    --spring.cloud.stream.bindings.output.producer.partitionSelectorName=customPartitionSelector
    +. . .
    +@Bean
    +public CustomPartitionSelectorClass customPartitionSelector() {
    +    return new CustomPartitionSelectorClass();
    +}
    [Note]Note

    In previous versions of Spring Cloud Stream you could specify the implementation of org.springframework.cloud.stream.binder.PartitionSelectorStrategy by setting the spring.cloud.stream.bindings.output.producer.partitionSelectorClass property. +Since version 2.0, this property is deprecated and support for it will be removed in a future version.

    34.3.2 Configuring Input Bindings for Partitioning

    An input binding (with the channel name input) is configured to receive partitioned data by setting its partitioned property, as well as the instanceIndex and instanceCount properties on the application itself, as shown in the following example:

    spring.cloud.stream.bindings.input.consumer.partitioned=true
    +spring.cloud.stream.instanceIndex=3
    +spring.cloud.stream.instanceCount=5

    The instanceCount value represents the total number of application instances between which the data should be partitioned. +The instanceIndex must be a unique value across the multiple instances, with a value between 0 and instanceCount - 1. +The instance index helps each application instance to identify the unique partition(s) from which it receives data. +It is required by binders using technology that does not support partitioning natively. +For example, with RabbitMQ, there is a queue for each partition, with the queue name containing the instance index. +With Kafka, if autoRebalanceEnabled is true (default), Kafka takes care of distributing partitions across instances, and these properties are not required. +If autoRebalanceEnabled is set to false, the instanceCount and instanceIndex are used by the binder to determine which partition(s) the instance subscribes to (you must have at least as many partitions as there are instances). +The binder allocates the partitions instead of Kafka. +This might be useful if you want messages for a particular partition to always go to the same instance. +When a binder configuration requires them, it is important to set both values correctly in order to ensure that all of the data is consumed and that the application instances receive mutually exclusive datasets.

    While a scenario in which using multiple instances for partitioned data processing may be complex to set up in a standalone case, Spring Cloud Dataflow can simplify the process significantly by populating both the input and output values correctly and by letting you rely on the runtime infrastructure to provide information about the instance index and instance count.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__introduction.html b/Greenwich.SR5/multi/multi__introduction.html new file mode 100644 index 00000000..676e170e --- /dev/null +++ b/Greenwich.SR5/multi/multi__introduction.html @@ -0,0 +1,291 @@ + + + 50. Introduction

    50. Introduction

    Spring Cloud Sleuth implements a distributed tracing solution for Spring Cloud.

    50.1 Terminology

    Spring Cloud Sleuth borrows Dapper’s terminology.

    Span: The basic unit of work. For example, sending an RPC is a new span, as is sending a response to an RPC. +Spans are identified by a unique 64-bit ID for the span and another 64-bit ID for the trace the span is a part of. +Spans also have other data, such as descriptions, timestamped events, key-value annotations (tags), the ID of the span that caused them, and process IDs (normally IP addresses).

    Spans can be started and stopped, and they keep track of their timing information. +Once you create a span, you must stop it at some point in the future.

    [Tip]Tip

    The initial span that starts a trace is called a root span. The value of the ID +of that span is equal to the trace ID.

    Trace: A set of spans forming a tree-like structure. +For example, if you run a distributed big-data store, a trace might be formed by a PUT request.

    Annotation: Used to record the existence of an event in time. With +Brave instrumentation, we no longer need to set special events +for Zipkin to understand who the client and server are, where +the request started, and where it ended. For learning purposes, +however, we mark these events to highlight what kind +of an action took place.

    • cs: Client Sent. The client has made a request. This annotation indicates the start of the span.
    • sr: Server Received: The server side got the request and started processing it. +Subtracting the cs timestamp from this timestamp reveals the network latency.
    • ss: Server Sent. Annotated upon completion of request processing (when the response got sent back to the client). +Subtracting the sr timestamp from this timestamp reveals the time needed by the server side to process the request.
    • cr: Client Received. Signifies the end of the span. +The client has successfully received the response from the server side. +Subtracting the cs timestamp from this timestamp reveals the whole time needed by the client to receive the response from the server.

    The following image shows how Span and Trace look in a system, together with the Zipkin annotations:

    Trace Info propagation

    Each color of a note signifies a span (there are seven spans - from A to G). +Consider the following note:

    Trace Id = X
    +Span Id = D
    +Client Sent

    This note indicates that the current span has Trace Id set to X and Span Id set to D. +Also, the Client Sent event took place.

    The following image shows how parent-child relationships of spans look:

    Parent child relationship

    50.2 Purpose

    The following sections refer to the example shown in the preceding image.

    50.2.1 Distributed Tracing with Zipkin

    This example has seven spans. +If you go to traces in Zipkin, you can see this number in the second trace, as shown in the following image:

    Traces

    However, if you pick a particular trace, you can see four spans, as shown in the following image:

    Traces Info propagation
    [Note]Note

    When you pick a particular trace, you see merged spans. +That means that, if there were two spans sent to Zipkin with Server Received and Server Sent or Client Received and Client Sent annotations, they are presented as a single span.

    Why is there a difference between the seven and four spans in this case?

    • One span comes from the http:/start span. It has the Server Received (sr) and Server Sent (ss) annotations.
    • Two spans come from the RPC call from service1 to service2 to the http:/foo endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service1 side. +Server Received (sr) and Server Sent (ss) events took place on the service2 side. +These two spans form one logical span related to an RPC call.
    • Two spans come from the RPC call from service2 to service3 to the http:/bar endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service2 side. +The Server Received (sr) and Server Sent (ss) events took place on the service3 side. +These two spans form one logical span related to an RPC call.
    • Two spans come from the RPC call from service2 to service4 to the http:/baz endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service2 side. +Server Received (sr) and Server Sent (ss) events took place on the service4 side. +These two spans form one logical span related to an RPC call.

    So, if we count the physical spans, we have one from http:/start, two from service1 calling service2, two from service2 +calling service3, and two from service2 calling service4. In sum, we have a total of seven spans.

    Logically, we see the information of four total Spans because we have one span related to the incoming request +to service1 and three spans related to RPC calls.

    50.2.2 Visualizing errors

    Zipkin lets you visualize errors in your trace. +When an exception was thrown and was not caught, we set proper tags on the span, which Zipkin can then properly colorize. +You could see in the list of traces one trace that is red. That appears because an exception was thrown.

    If you click that trace, you see a similar picture, as follows:

    Error Traces

    If you then click on one of the spans, you see the following

    Error Traces Info propagation

    The span shows the reason for the error and the whole stack trace related to it.

    50.2.3 Distributed Tracing with Brave

    Starting with version 2.0.0, Spring Cloud Sleuth uses Brave as the tracing library. +Consequently, Sleuth no longer takes care of storing the context but delegates that work to Brave.

    Due to the fact that Sleuth had different naming and tagging conventions than Brave, we decided to follow Brave’s conventions from now on. +However, if you want to use the legacy Sleuth approaches, you can set the spring.sleuth.http.legacy.enabled property to true.

    50.2.4 Live examples

    Figure 50.1. Click the Pivotal Web Services icon to see it live!

    Zipkin deployed on Pivotal Web Services

    Click here to see it live!

    The dependency graph in Zipkin should resemble the following image:

    Dependencies

    Figure 50.2. Click the Pivotal Web Services icon to see it live!

    Zipkin deployed on Pivotal Web Services

    Click here to see it live!

    50.2.5 Log correlation

    When using grep to read the logs of those four applications by scanning for a trace ID equal to (for example) 2485ec27856c56f4, you get output resembling the following:

    service1.log:2016-02-26 11:15:47.561  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Hello from service1. Calling service2
    +service2.log:2016-02-26 11:15:47.710  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Hello from service2. Calling service3 and then service4
    +service3.log:2016-02-26 11:15:47.895  INFO [service3,2485ec27856c56f4,1210be13194bfe5,true] 68060 --- [nio-8083-exec-1] i.s.c.sleuth.docs.service3.Application   : Hello from service3
    +service2.log:2016-02-26 11:15:47.924  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service3 [Hello from service3]
    +service4.log:2016-02-26 11:15:48.134  INFO [service4,2485ec27856c56f4,1b1845262ffba49d,true] 68061 --- [nio-8084-exec-1] i.s.c.sleuth.docs.service4.Application   : Hello from service4
    +service2.log:2016-02-26 11:15:48.156  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service4 [Hello from service4]
    +service1.log:2016-02-26 11:15:48.182  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Got response from service2 [Hello from service2, response from service3 [Hello from service3] and from service4 [Hello from service4]]

    If you use a log aggregating tool (such as Kibana, Splunk, and others), you can order the events that took place. +An example from Kibana would resemble the following image:

    Log correlation with Kibana

    If you want to use Logstash, the following listing shows the Grok pattern for Logstash:

    filter {
    +       # pattern matching logback pattern
    +       grok {
    +              match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
    +       }
    +}
    [Note]Note

    If you want to use Grok together with the logs from Cloud Foundry, you have to use the following pattern:

    filter {
    +       # pattern matching logback pattern
    +       grok {
    +              match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
    +       }
    +}

    JSON Logback with Logstash

    Often, you do not want to store your logs in a text file but in a JSON file that Logstash can immediately pick. +To do so, you have to do the following (for readability, we pass the dependencies in the groupId:artifactId:version notation).

    Dependencies Setup

    1. Ensure that Logback is on the classpath (ch.qos.logback:logback-core).
    2. Add Logstash Logback encode. For example, to use version 4.6, add net.logstash.logback:logstash-logback-encoder:4.6.

    Logback Setup

    Consider the following example of a Logback configuration file (named logback-spring.xml).

    <?xml version="1.0" encoding="UTF-8"?>
    +<configuration>
    +	<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    +	​
    +	<springProperty scope="context" name="springAppName" source="spring.application.name"/>
    +	<!-- Example for logging into the build folder of your project -->
    +	<property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>​
    +
    +	<!-- You can override this to have a custom pattern -->
    +	<property name="CONSOLE_LOG_PATTERN"
    +			  value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    +
    +	<!-- Appender to log to console -->
    +	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    +		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    +			<!-- Minimum logging level to be presented in the console logs-->
    +			<level>DEBUG</level>
    +		</filter>
    +		<encoder>
    +			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
    +			<charset>utf8</charset>
    +		</encoder>
    +	</appender>
    +
    +	<!-- Appender to log to file -->​
    +	<appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
    +		<file>${LOG_FILE}</file>
    +		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    +			<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
    +			<maxHistory>7</maxHistory>
    +		</rollingPolicy>
    +		<encoder>
    +			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
    +			<charset>utf8</charset>
    +		</encoder>
    +	</appender>
    +	​
    +	<!-- Appender to log to file in a JSON format -->
    +	<appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
    +		<file>${LOG_FILE}.json</file>
    +		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    +			<fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
    +			<maxHistory>7</maxHistory>
    +		</rollingPolicy>
    +		<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
    +			<providers>
    +				<timestamp>
    +					<timeZone>UTC</timeZone>
    +				</timestamp>
    +				<pattern>
    +					<pattern>
    +						{
    +						"severity": "%level",
    +						"service": "${springAppName:-}",
    +						"trace": "%X{X-B3-TraceId:-}",
    +						"span": "%X{X-B3-SpanId:-}",
    +						"parent": "%X{X-B3-ParentSpanId:-}",
    +						"exportable": "%X{X-Span-Export:-}",
    +						"pid": "${PID:-}",
    +						"thread": "%thread",
    +						"class": "%logger{40}",
    +						"rest": "%message"
    +						}
    +					</pattern>
    +				</pattern>
    +			</providers>
    +		</encoder>
    +	</appender>
    +	​
    +	<root level="INFO">
    +		<appender-ref ref="console"/>
    +		<!-- uncomment this to have also JSON logs -->
    +		<!--<appender-ref ref="logstash"/>-->
    +		<!--<appender-ref ref="flatfile"/>-->
    +	</root>
    +</configuration>

    That Logback configuration file:

    • Logs information from the application in a JSON format to a build/${spring.application.name}.json file.
    • Has commented out two additional appenders: console and standard log file.
    • Has the same logging pattern as the one presented in the previous section.
    [Note]Note

    If you use a custom logback-spring.xml, you must pass the spring.application.name in the bootstrap rather than the application property file. +Otherwise, your custom logback file does not properly read the property.

    50.2.6 Propagating Span Context

    The span context is the state that must get propagated to any child spans across process boundaries. +Part of the Span Context is the Baggage. The trace and span IDs are a required part of the span context. +Baggage is an optional part.

    Baggage is a set of key:value pairs stored in the span context. +Baggage travels together with the trace and is attached to every span. +Spring Cloud Sleuth understands that a header is baggage-related if the HTTP header is prefixed with baggage- and, for messaging, it starts with baggage_.

    [Important]Important

    There is currently no limitation of the count or size of baggage items. +However, keep in mind that too many can decrease system throughput or increase RPC latency. +In extreme cases, too much baggage can crash the application, due to exceeding transport-level message or header capacity.

    The following example shows setting baggage on a span:

    Span initialSpan = this.tracer.nextSpan().name("span").start();
    +ExtraFieldPropagation.set(initialSpan.context(), "foo", "bar");
    +ExtraFieldPropagation.set(initialSpan.context(), "UPPER_CASE", "someValue");

    Baggage versus Span Tags

    Baggage travels with the trace (every child span contains the baggage of its parent). +Zipkin has no knowledge of baggage and does not receive that information.

    [Important]Important

    Starting from Sleuth 2.0.0 you have to pass the baggage key names explicitly +in your project configuration. Read more about that setup here

    Tags are attached to a specific span. In other words, they are presented only for that particular span. +However, you can search by tag to find the trace, assuming a span having the searched tag value exists.

    If you want to be able to lookup a span based on baggage, you should add a corresponding entry as a tag in the root span.

    [Important]Important

    The span must be in scope.

    The following listing shows integration tests that use baggage:

    The setup.  +

    spring.sleuth:
    +  baggage-keys:
    +    - baz
    +    - bizarrecase
    +  propagation-keys:
    +    - foo
    +    - upper_case

    +

    The code.  +

    initialSpan.tag("foo",
    +		ExtraFieldPropagation.get(initialSpan.context(), "foo"));
    +initialSpan.tag("UPPER_CASE",
    +		ExtraFieldPropagation.get(initialSpan.context(), "UPPER_CASE"));

    +

    50.3 Adding Sleuth to the Project

    This section addresses how to add Sleuth to your project with either Maven or Gradle.

    [Important]Important

    To ensure that your application name is properly displayed in Zipkin, set the spring.application.name property in bootstrap.yml.

    50.3.1 Only Sleuth (log correlation)

    If you want to use only Spring Cloud Sleuth without the Zipkin integration, add the spring-cloud-starter-sleuth module to your project.

    The following example shows how to add Sleuth with Maven:

    Maven.  +

    <dependencyManagement> 1
    +      <dependencies>
    +          <dependency>
    +              <groupId>org.springframework.cloud</groupId>
    +              <artifactId>spring-cloud-dependencies</artifactId>
    +              <version>${release.train.version}</version>
    +              <type>pom</type>
    +              <scope>import</scope>
    +          </dependency>
    +      </dependencies>
    +</dependencyManagement>
    +
    +<dependency> 2
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-sleuth</artifactId>
    +</dependency>

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-sleuth.

    The following example shows how to add Sleuth with Gradle:

    Gradle.  +

    dependencyManagement { 1
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    +    }
    +}
    +
    +dependencies { 2
    +    compile "org.springframework.cloud:spring-cloud-starter-sleuth"
    +}

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-sleuth.

    50.3.2 Sleuth with Zipkin via HTTP

    If you want both Sleuth and Zipkin, add the spring-cloud-starter-zipkin dependency.

    The following example shows how to do so for Maven:

    Maven.  +

    <dependencyManagement> 1
    +      <dependencies>
    +          <dependency>
    +              <groupId>org.springframework.cloud</groupId>
    +              <artifactId>spring-cloud-dependencies</artifactId>
    +              <version>${release.train.version}</version>
    +              <type>pom</type>
    +              <scope>import</scope>
    +          </dependency>
    +      </dependencies>
    +</dependencyManagement>
    +
    +<dependency> 2
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-zipkin</artifactId>
    +</dependency>

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin.

    The following example shows how to do so for Gradle:

    Gradle.  +

    dependencyManagement { 1
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    +    }
    +}
    +
    +dependencies { 2
    +    compile "org.springframework.cloud:spring-cloud-starter-zipkin"
    +}

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin.

    50.3.3 Sleuth with Zipkin over RabbitMQ or Kafka

    If you want to use RabbitMQ or Kafka instead of HTTP, add the spring-rabbit or spring-kafka dependency. +The default destination name is zipkin.

    If using Kafka, you must set the property spring.zipkin.sender.type property accordingly:

    spring.zipkin.sender.type: kafka
    [Caution]Caution

    spring-cloud-sleuth-stream is deprecated and incompatible with these destinations.

    If you want Sleuth over RabbitMQ, add the spring-cloud-starter-zipkin and spring-rabbit +dependencies.

    The following example shows how to do so for Gradle:

    Maven.  +

    <dependencyManagement> 1
    +      <dependencies>
    +          <dependency>
    +              <groupId>org.springframework.cloud</groupId>
    +              <artifactId>spring-cloud-dependencies</artifactId>
    +              <version>${release.train.version}</version>
    +              <type>pom</type>
    +              <scope>import</scope>
    +          </dependency>
    +      </dependencies>
    +</dependencyManagement>
    +
    +<dependency> 2
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-zipkin</artifactId>
    +</dependency>
    +<dependency> 3
    +    <groupId>org.springframework.amqp</groupId>
    +    <artifactId>spring-rabbit</artifactId>
    +</dependency>

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin. That way, all nested dependencies get downloaded.

    3

    To automatically configure RabbitMQ, add the spring-rabbit dependency.

    Gradle.  +

    dependencyManagement { 1
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    +    }
    +}
    +
    +dependencies {
    +    compile "org.springframework.cloud:spring-cloud-starter-zipkin" 2
    +    compile "org.springframework.amqp:spring-rabbit" 3
    +}

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin. That way, all nested dependencies get downloaded.

    3

    To automatically configure RabbitMQ, add the spring-rabbit dependency.

    50.4 Overriding the auto-configuration of Zipkin

    Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. +In order to get this to work, every tracing system needs to have a Reporter<Span> and Sender. +If you want to override the provided beans you need to give them a specific name. +To do this you can use respectively ZipkinAutoConfiguration.REPORTER_BEAN_NAME and ZipkinAutoConfiguration.SENDER_BEAN_NAME.

    @Configuration
    +protected static class MyConfig {
    +
    +	@Bean(ZipkinAutoConfiguration.REPORTER_BEAN_NAME)
    +	Reporter<zipkin2.Span> myReporter() {
    +		return AsyncReporter.create(mySender());
    +	}
    +
    +	@Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME)
    +	MySender mySender() {
    +		return new MySender();
    +	}
    +
    +	static class MySender extends Sender {
    +
    +		private boolean spanSent = false;
    +
    +		boolean isSpanSent() {
    +			return this.spanSent;
    +		}
    +
    +		@Override
    +		public Encoding encoding() {
    +			return Encoding.JSON;
    +		}
    +
    +		@Override
    +		public int messageMaxBytes() {
    +			return Integer.MAX_VALUE;
    +		}
    +
    +		@Override
    +		public int messageSizeInBytes(List<byte[]> encodedSpans) {
    +			return encoding().listSizeInBytes(encodedSpans);
    +		}
    +
    +		@Override
    +		public Call<Void> sendSpans(List<byte[]> encodedSpans) {
    +			this.spanSent = true;
    +			return Call.create(null);
    +		}
    +
    +	}
    +
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__introduction_2.html b/Greenwich.SR5/multi/multi__introduction_2.html new file mode 100644 index 00000000..2e336c81 --- /dev/null +++ b/Greenwich.SR5/multi/multi__introduction_2.html @@ -0,0 +1,27 @@ + + + 126. Introduction

    126. Introduction

    Spring Cloud Function is a project with the following high-level goals:

    • Promote the implementation of business logic via functions.
    • Decouple the development lifecycle of business logic from any specific runtime target so that the same code can run as a web endpoint, a stream processor, or a task.
    • Support a uniform programming model across serverless providers, as well as the ability to run standalone (locally or in a PaaS).
    • Enable Spring Boot features (auto-configuration, dependency injection, metrics) on serverless providers.

    It abstracts away all of the transport details and +infrastructure, allowing the developer to keep all the familiar tools +and processes, and focus firmly on business logic.

    Here’s a complete, executable, testable Spring Boot application +(implementing a simple string manipulation):

    @SpringBootApplication
    +public class Application {
    +
    +  @Bean
    +  public Function<Flux<String>, Flux<String>> uppercase() {
    +    return flux -> flux.map(value -> value.toUpperCase());
    +  }
    +
    +  public static void main(String[] args) {
    +    SpringApplication.run(Application.class, args);
    +  }
    +}

    It’s just a Spring Boot application, so it can be built, run and +tested, locally and in a CI build, the same way as any other Spring +Boot application. The Function is from java.util and Flux is a +Reactive Streams Publisher from +Project Reactor. The function can be +accessed over HTTP or messaging.

    Spring Cloud Function has 4 main features:

    1. Wrappers for @Beans of type Function, Consumer and +Supplier, exposing them to the outside world as either HTTP +endpoints and/or message stream listeners/publishers with RabbitMQ, Kafka etc.
    2. Compiling strings which are Java function bodies into bytecode, and +then turning them into @Beans that can be wrapped as above.
    3. Deploying a JAR file containing such an application context with an +isolated classloader, so that you can pack them together in a single +JVM.
    4. Adapters for AWS Lambda, Azure, Apache OpenWhisk and possibly other "serverless" service providers.
    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__introduction_4.html b/Greenwich.SR5/multi/multi__introduction_4.html new file mode 100644 index 00000000..bc75fbfd --- /dev/null +++ b/Greenwich.SR5/multi/multi__introduction_4.html @@ -0,0 +1,3 @@ + + + 151. Introduction

    151. Introduction

    The Spring Cloud GCP project makes the Spring Framework a first-class citizen of Google Cloud Platform (GCP).

    Spring Cloud GCP lets you leverage the power and simplicity of the Spring Framework to:

    1. Analyze your images for text, objects, and other content with Google Cloud Vision
    2. Use Spring Security via Google Cloud IAP
    3. Map objects, relationships, and collections with Spring Data Cloud Spanner and Spring Data Cloud Datastore
    4. Publish and subscribe to Google Cloud Pub/Sub topics
    5. Configure Spring JDBC with a few properties to use Google Cloud SQL
    6. Write and read from Spring Resources backed up by Google Cloud Storage
    7. Exchange messages with Spring Integration using Google Cloud Pub/Sub on the background
    8. Trace the execution of your app with Spring Cloud Sleuth and Google Stackdriver Trace
    9. Configure your app with Spring Cloud Config, backed up by the Google Runtime Configuration API
    10. Consume and produce Google Cloud Storage data via Spring Integration GCS Channel Adapters
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__kotlin_support.html b/Greenwich.SR5/multi/multi__kotlin_support.html new file mode 100644 index 00000000..2f61ca18 --- /dev/null +++ b/Greenwich.SR5/multi/multi__kotlin_support.html @@ -0,0 +1,5 @@ + + + 169. Kotlin Support

    169. Kotlin Support

    The latest version of the Spring Framework provides first-class support for Kotlin. +For Kotlin users of Spring, the Spring Cloud GCP libraries work out-of-the-box and are fully interoperable with Kotlin applications.

    For more information on building a Spring application in Kotlin, please consult the Spring Kotlin documentation.

    169.1 Prerequisites

    Ensure that your Kotlin application is properly set up. +Based on your build system, you will need to include the correct Kotlin build plugin in your project:

    Depending on your application’s needs, you may need to augment your build configuration with compiler plugins:

    Once your Kotlin project is properly configured, the Spring Cloud GCP libraries will work within your application without any additional setup.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__kubernetes_ecosystem_awareness.html b/Greenwich.SR5/multi/multi__kubernetes_ecosystem_awareness.html new file mode 100644 index 00000000..83cdb8c3 --- /dev/null +++ b/Greenwich.SR5/multi/multi__kubernetes_ecosystem_awareness.html @@ -0,0 +1,16 @@ + + + 142. Kubernetes Ecosystem Awareness

    142. Kubernetes Ecosystem Awareness

    All of the features described earlier in this guide work equally well, regardless of whether your application is running inside +Kubernetes. This is really helpful for development and troubleshooting. +From a development point of view, this lets you start your Spring Boot application and debug one +of the modules that is part of this project. You need not deploy it in Kubernetes, +as the code of the project relies on the +Fabric8 Kubernetes Java client, which is a fluent DSL that can +communicate by using http protocol to the REST API of the Kubernetes Server.

    To disable the integration with Kubernetes you can set spring.cloud.kubernetes.enabled to false. Please be aware that when spring-cloud-kubernetes-config is on the classpath, +spring.cloud.kubernetes.enabled should be set in bootstrap.{properties|yml} (or the profile specific one) otherwise it should be in application.{properties|yml} (or the profile specific one). +Also note that these properties: spring.cloud.kubernetes.config.enabled and spring.cloud.kubernetes.secrets.enabled only take effect when set in bootstrap.{properties|yml}

    142.1 Kubernetes Profile Autoconfiguration

    When the application runs as a pod inside Kubernetes, a Spring profile named kubernetes automatically gets activated. +This lets you customize the configuration, to define beans that are applied when the Spring Boot application is deployed +within the Kubernetes platform (for example, different development and production configuration).

    142.2 Istio Awareness

    When you include the spring-cloud-kubernetes-istio module in the application classpath, a new profile is added to the application, +provided the application is running inside a Kubernetes Cluster with Istio installed. You can then use +spring @Profile("istio") annotations in your Beans and @Configuration classes.

    The Istio awareness module uses me.snowdrop:istio-client to interact with Istio APIs, letting us discover traffic rules, circuit breakers, and so on, +making it easy for our Spring Boot applications to consume this data to dynamically configure themselves according to the environment.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__kubernetes_native_service_discovery.html b/Greenwich.SR5/multi/multi__kubernetes_native_service_discovery.html new file mode 100644 index 00000000..ecc4bc74 --- /dev/null +++ b/Greenwich.SR5/multi/multi__kubernetes_native_service_discovery.html @@ -0,0 +1,4 @@ + + + 139. Kubernetes native service discovery

    139. Kubernetes native service discovery

    Kubernetes itself is capable of (server side) service discovery (see: https://kubernetes.io/docs/concepts/services-networking/service/#discovering-services). +Using native kubernetes service discovery ensures compatibility with additional tooling, such as Istio (https://istio.io), a service mesh that is capable of load balancing, ribbon, circuit breaker, failover, and much more.

    The caller service then need only refer to names resolvable in a particular Kubernetes cluster. A simple implementation might use a spring RestTemplate that refers to a fully qualified domain name (FQDN), such as https://{service-name}.{namespace}.svc.{cluster}.local:{service-port}.

    Additionally, you can use Hystrix for:

    • Circuit breaker implementation on the caller side, by annotating the spring boot application class with @EnableCircuitBreaker
    • Fallback functionality, by annotating the respective method with @HystrixCommand(fallbackMethod=
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__kubernetes_propertysource_implementations.html b/Greenwich.SR5/multi/multi__kubernetes_propertysource_implementations.html new file mode 100644 index 00000000..0441e111 --- /dev/null +++ b/Greenwich.SR5/multi/multi__kubernetes_propertysource_implementations.html @@ -0,0 +1,223 @@ + + + 140. Kubernetes PropertySource implementations

    140. Kubernetes PropertySource implementations

    The most common approach to configuring your Spring Boot application is to create an application.properties or applicaiton.yaml or +an application-profile.properties or application-profile.yaml file that contains key-value pairs that provide customization values to your +application or Spring Boot starters. You can override these properties by specifying system properties or environment +variables.

    140.1 Using a ConfigMap PropertySource

    Kubernetes provides a resource named ConfigMap to externalize the +parameters to pass to your application in the form of key-value pairs or embedded application.properties or application.yaml files. +The Spring Cloud Kubernetes Config project makes Kubernetes ConfigMap instances available +during application bootstrapping and triggers hot reloading of beans or Spring context when changes are detected on +observed ConfigMap instances.

    The default behavior is to create a ConfigMapPropertySource based on a Kubernetes ConfigMap that has a metadata.name value of either the name of +your Spring application (as defined by its spring.application.name property) or a custom name defined within the +bootstrap.properties file under the following key: spring.cloud.kubernetes.config.name.

    However, more advanced configuration is possible where you can use multiple ConfigMap instances. +The spring.cloud.kubernetes.config.sources list makes this possible. +For example, you could define the following ConfigMap instances:

    spring:
    +  application:
    +    name: cloud-k8s-app
    +  cloud:
    +    kubernetes:
    +      config:
    +        name: default-name
    +        namespace: default-namespace
    +        sources:
    +         # Spring Cloud Kubernetes looks up a ConfigMap named c1 in namespace default-namespace
    +         - name: c1
    +         # Spring Cloud Kubernetes looks up a ConfigMap named default-name in whatever namespace n2
    +         - namespace: n2
    +         # Spring Cloud Kubernetes looks up a ConfigMap named c3 in namespace n3
    +         - namespace: n3
    +           name: c3

    In the preceding example, if spring.cloud.kubernetes.config.namespace had not been set, +the ConfigMap named c1 would be looked up in the namespace that the application runs.

    Any matching ConfigMap that is found is processed as follows:

    • Apply individual configuration properties.
    • Apply as yaml the content of any property named application.yaml.
    • Apply as a properties file the content of any property named application.properties.

    The single exception to the aforementioned flow is when the ConfigMap contains a single key that indicates +the file is a YAML or properties file. In that case, the name of the key does NOT have to be application.yaml or +application.properties (it can be anything) and the value of the property is treated correctly. +This features facilitates the use case where the ConfigMap was created by using something like the following:

    kubectl create configmap game-config --from-file=/path/to/app-config.yaml

    Assume that we have a Spring Boot application named demo that uses the following properties to read its thread pool +configuration.

    • pool.size.core
    • pool.size.maximum

    This can be externalized to config map in yaml format as follows:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  pool.size.core: 1
    +  pool.size.max: 16

    Individual properties work fine for most cases. However, sometimes, embedded yaml is more convenient. In this case, we +use a single property named application.yaml to embed our yaml, as follows:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  application.yaml: |-
    +    pool:
    +      size:
    +        core: 1
    +        max:16

    The following example also works:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  custom-name.yaml: |-
    +    pool:
    +      size:
    +        core: 1
    +        max:16

    You can also configure Spring Boot applications differently depending on active profiles that are merged together +when the ConfigMap is read. You can provide different property values for different profiles by using an +application.properties or application.yaml property, specifying profile-specific values, each in their own document +(indicated by the --- sequence), as follows:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  application.yml: |-
    +    greeting:
    +      message: Say Hello to the World
    +    farewell:
    +      message: Say Goodbye
    +    ---
    +    spring:
    +      profiles: development
    +    greeting:
    +      message: Say Hello to the Developers
    +    farewell:
    +      message: Say Goodbye to the Developers
    +    ---
    +    spring:
    +      profiles: production
    +    greeting:
    +      message: Say Hello to the Ops

    In the preceding case, the configuration loaded into your Spring Application with the development profile is as follows:

      greeting:
    +    message: Say Hello to the Developers
    +  farewell:
    +    message: Say Goodbye to the Developers

    However, if the production profile is active, the configuration becomes:

      greeting:
    +    message: Say Hello to the Ops
    +  farewell:
    +    message: Say Goodbye

    If both profiles are active, the property that appears last within the ConfigMap overwrites any preceding values.

    Another option is to create a different config map per profile and spring boot will automatically fetch it based +on active profiles

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  application.yml: |-
    +    greeting:
    +      message: Say Hello to the World
    +    farewell:
    +      message: Say Goodbye
    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo-development
    +data:
    +  application.yml: |-
    +    spring:
    +      profiles: development
    +    greeting:
    +      message: Say Hello to the Developers
    +    farewell:
    +      message: Say Goodbye to the Developers
    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo-production
    +data:
    +  application.yml: |-
    +    spring:
    +      profiles: production
    +    greeting:
    +      message: Say Hello to the Ops
    +    farewell:
    +      message: Say Goodbye

    To tell Spring Boot which profile should be enabled at bootstrap, you can pass a system property to the Java +command. To do so, you can launch your Spring Boot application with an environment variable that you can define with the OpenShift +DeploymentConfig or Kubernetes ReplicationConfig resource file, as follows:

    apiVersion: v1
    +kind: DeploymentConfig
    +spec:
    +  replicas: 1
    +  ...
    +    spec:
    +      containers:
    +      - env:
    +        - name: JAVA_APP_DIR
    +          value: /deployments
    +        - name: JAVA_OPTIONS
    +          value: -Dspring.profiles.active=developer
    [Note]Note

    You should check the security configuration section. To access config maps from inside a pod you need to have the correct +Kubernetes service accounts, roles and role bindings.

    Another option for using ConfigMap instances is to mount them into the Pod by running the Spring Cloud Kubernetes application +and having Spring Cloud Kubernetes read them from the file system. +This behavior is controlled by the spring.cloud.kubernetes.config.paths property. You can use it in +addition to or instead of the mechanism described earlier. +You can specify multiple (exact) file paths in spring.cloud.kubernetes.config.paths by using the , delimiter.

    [Note]Note

    You have to provide the full exact path to each property file, because directories are not being recursively parsed.

    Table 140.1. Properties:

    NameTypeDefaultDescription

    spring.cloud.kubernetes.config.enableApi

    Boolean

    true

    Enable or disable consuming ConfigMap instances through APIs

    spring.cloud.kubernetes.config.enabled

    Boolean

    true

    Enable ConfigMaps PropertySource

    spring.cloud.kubernetes.config.name

    String

    ${spring.application.name}

    Sets the name of ConfigMap to look up

    spring.cloud.kubernetes.config.namespace

    String

    Client namespace

    Sets the Kubernetes namespace where to lookup

    spring.cloud.kubernetes.config.paths

    List

    null

    Sets the paths where ConfigMap instances are mounted


    140.2 Secrets PropertySource

    Kubernetes has the notion of Secrets for storing +sensitive data such as passwords, OAuth tokens, and so on. This project provides integration with Secrets to make secrets +accessible by Spring Boot applications. You can explicitly enable or disable This feature by setting the spring.cloud.kubernetes.secrets.enabled property.

    When enabled, the SecretsPropertySource looks up Kubernetes for Secrets from the following sources:

    1. Reading recursively from secrets mounts
    2. Named after the application (as defined by spring.application.name)
    3. Matching some labels

    Note:

    By default, consuming Secrets through the API (points 2 and 3 above) is not enabled for security reasons. The permission 'list' on secrets allows clients to inspect secrets values in the specified namespace. +Further, we recommend that containers share secrets through mounted volumes.

    If you enable consuming Secrets through the API, we recommend that you limit access to Secrets by using an authorization policy, such as RBAC. +For more information about risks and best practices when consuming Secrets through the API refer to this doc.

    If the secrets are found, their data is made available to the application.

    Assume that we have a spring boot application named demo that uses properties to read its database +configuration. We can create a Kubernetes secret by using the following command:

    oc create secret generic db-secret --from-literal=username=user --from-literal=password=p455w0rd

    The preceding command would create the following secret (which you can see by using oc get secrets db-secret -o yaml):

    apiVersion: v1
    +data:
    +  password: cDQ1NXcwcmQ=
    +  username: dXNlcg==
    +kind: Secret
    +metadata:
    +  creationTimestamp: 2017-07-04T09:15:57Z
    +  name: db-secret
    +  namespace: default
    +  resourceVersion: "357496"
    +  selfLink: /api/v1/namespaces/default/secrets/db-secret
    +  uid: 63c89263-6099-11e7-b3da-76d6186905a8
    +type: Opaque

    Note that the data contains Base64-encoded versions of the literal provided by the create command.

    Your application can then use this secret — for example, by exporting the secret’s value as environment variables:

    apiVersion: v1
    +kind: Deployment
    +metadata:
    +  name: ${project.artifactId}
    +spec:
    +   template:
    +     spec:
    +       containers:
    +         - env:
    +            - name: DB_USERNAME
    +              valueFrom:
    +                 secretKeyRef:
    +                   name: db-secret
    +                   key: username
    +            - name: DB_PASSWORD
    +              valueFrom:
    +                 secretKeyRef:
    +                   name: db-secret
    +                   key: password

    You can select the Secrets to consume in a number of ways:

    1. By listing the directories where secrets are mapped:

      -Dspring.cloud.kubernetes.secrets.paths=/etc/secrets/db-secret,etc/secrets/postgresql

      If you have all the secrets mapped to a common root, you can set them like:

      -Dspring.cloud.kubernetes.secrets.paths=/etc/secrets
    2. By setting a named secret:

      -Dspring.cloud.kubernetes.secrets.name=db-secret
    3. By defining a list of labels:

      -Dspring.cloud.kubernetes.secrets.labels.broker=activemq
      +-Dspring.cloud.kubernetes.secrets.labels.db=postgresql

    Table 140.2. Properties:

    NameTypeDefaultDescription

    spring.cloud.kubernetes.secrets.enableApi

    Boolean

    false

    Enables or disables consuming secrets through APIs (examples 2 and 3)

    spring.cloud.kubernetes.secrets.enabled

    Boolean

    true

    Enable Secrets PropertySource

    spring.cloud.kubernetes.secrets.name

    String

    ${spring.application.name}

    Sets the name of the secret to look up

    spring.cloud.kubernetes.secrets.namespace

    String

    Client namespace

    Sets the Kubernetes namespace where to look up

    spring.cloud.kubernetes.secrets.labels

    Map

    null

    Sets the labels used to lookup secrets

    spring.cloud.kubernetes.secrets.paths

    List

    null

    Sets the paths where secrets are mounted (example 1)


    Notes:

    • The spring.cloud.kubernetes.secrets.labels property behaves as defined by +Map-based binding.
    • The spring.cloud.kubernetes.secrets.paths property behaves as defined by +Collection-based binding.
    • Access to secrets through the API may be restricted for security reasons. The preferred way is to mount secrets to the Pod.

    You can find an example of an application that uses secrets (though it has not been updated to use the new spring-cloud-kubernetes project) at +spring-boot-camel-config

    140.3 PropertySource Reload

    Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. +The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related ConfigMap or +Secret changes.

    By default, this feature is disabled. You can enable it by using the spring.cloud.kubernetes.reload.enabled=true configuration property (for example, in the application.properties file).

    The following levels of reload are supported (by setting the spring.cloud.kubernetes.reload.strategy property): +* refresh (default): Only configuration beans annotated with @ConfigurationProperties or @RefreshScope are reloaded. +This reload level leverages the refresh feature of Spring Cloud Context. +* restart_context: the whole Spring ApplicationContext is gracefully restarted. Beans are recreated with the new configuration. +* shutdown: the Spring ApplicationContext is shut down to activate a restart of the container. + When you use this level, make sure that the lifecycle of all non-daemon threads is bound to the ApplicationContext +and that a replication controller or replica set is configured to restart the pod.

    Assuming that the reload feature is enabled with default settings (refresh mode), the following bean is refreshed when the config map changes:

    @Configuration
    +@ConfigurationProperties(prefix = "bean")
    +public class MyConfig {
    +
    +    private String message = "a message that can be changed live";
    +
    +    // getter and setters
    +
    +}

    To see that changes effectively happen, you can create another bean that prints the message periodically, as follows

    @Component
    +public class MyBean {
    +
    +    @Autowired
    +    private MyConfig config;
    +
    +    @Scheduled(fixedDelay = 5000)
    +    public void hello() {
    +        System.out.println("The message is: " + config.getMessage());
    +    }
    +}

    You can change the message printed by the application by using a ConfigMap, as follows:

    apiVersion: v1
    +kind: ConfigMap
    +metadata:
    +  name: reload-example
    +data:
    +  application.properties: |-
    +    bean.message=Hello World!

    Any change to the property named bean.message in the ConfigMap associated with the pod is reflected in the +output. More generally speaking, changes associated to properties prefixed with the value defined by the prefix +field of the @ConfigurationProperties annotation are detected and reflected in the application. +Associating a ConfigMap with a pod is explained earlier in this chapter.

    The full example is available in spring-cloud-kubernetes-reload-example.

    The reload feature supports two operating modes: +* Event (default): Watches for changes in config maps or secrets by using the Kubernetes API (web socket). +Any event produces a re-check on the configuration and, in case of changes, a reload. +The view role on the service account is required in order to listen for config map changes. A higher level role (such as edit) is required for secrets +(by default, secrets are not monitored). +* Polling: Oeriodically re-creates the configuration from config maps and secrets to see if it has changed. +You can configure the polling period by using the spring.cloud.kubernetes.reload.period property and defaults to 15 seconds. +It requires the same role as the monitored property source. +This means, for example, that using polling on file-mounted secret sources does not require particular privileges.

    Table 140.3. Properties:

    NameTypeDefaultDescription

    spring.cloud.kubernetes.reload.period

    Duration

    15s

    The period for verifying changes when using the polling strategy

    spring.cloud.kubernetes.reload.enabled

    Boolean

    false

    Enables monitoring of property sources and configuration reload

    spring.cloud.kubernetes.reload.monitoring-config-maps

    Boolean

    true

    Allow monitoring changes in config maps

    spring.cloud.kubernetes.reload.monitoring-secrets

    Boolean

    false

    Allow monitoring changes in secrets

    `spring.cloud.kubernetes.reload.strategy `

    Enum

    refresh

    The strategy to use when firing a reload (refresh, restart_context, or shutdown)

    spring.cloud.kubernetes.reload.mode

    Enum

    event

    Specifies how to listen for changes in property sources (event or polling)


    Notes: +* You should not use properties under spring.cloud.kubernetes.reload in config maps or secrets. Changing such properties at runtime may lead to unexpected results. +* Deleting a property or the whole config map does not restore the original state of the beans when you use the refresh level.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__leader_election.html b/Greenwich.SR5/multi/multi__leader_election.html new file mode 100644 index 00000000..5c263789 --- /dev/null +++ b/Greenwich.SR5/multi/multi__leader_election.html @@ -0,0 +1,3 @@ + + + 144. Leader Election

    144. Leader Election

    <TBD>

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__links.html b/Greenwich.SR5/multi/multi__links.html new file mode 100644 index 00000000..548f9c57 --- /dev/null +++ b/Greenwich.SR5/multi/multi__links.html @@ -0,0 +1,6 @@ + + + 99. Links \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__main_concepts.html b/Greenwich.SR5/multi/multi__main_concepts.html new file mode 100644 index 00000000..882cf81f --- /dev/null +++ b/Greenwich.SR5/multi/multi__main_concepts.html @@ -0,0 +1,35 @@ + + + 28. Main Concepts

    28. Main Concepts

    Spring Cloud Stream provides a number of abstractions and primitives that simplify the writing of message-driven microservice applications. +This section gives an overview of the following:

    28.1 Application Model

    A Spring Cloud Stream application consists of a middleware-neutral core. +The application communicates with the outside world through input and output channels injected into it by Spring Cloud Stream. +Channels are connected to external brokers through middleware-specific Binder implementations.

    Figure 28.1. Spring Cloud Stream Application

    SCSt with binder

    28.1.1 Fat JAR

    Spring Cloud Stream applications can be run in stand-alone mode from your IDE for testing. +To run a Spring Cloud Stream application in production, you can create an executable (or fat) JAR by using the standard Spring Boot tooling provided for Maven or Gradle. See the Spring Boot Reference Guide for more details.

    28.2 The Binder Abstraction

    Spring Cloud Stream provides Binder implementations for Kafka and Rabbit MQ. +Spring Cloud Stream also includes a TestSupportBinder, which leaves a channel unmodified so that tests can interact with channels directly and reliably assert on what is received. +You can also use the extensible API to write your own Binder.

    Spring Cloud Stream uses Spring Boot for configuration, and the Binder abstraction makes it possible for a Spring Cloud Stream application to be flexible in how it connects to middleware. +For example, deployers can dynamically choose, at runtime, the destinations (such as the Kafka topics or RabbitMQ exchanges) to which channels connect. +Such configuration can be provided through external configuration properties and in any form supported by Spring Boot (including application arguments, environment variables, and application.yml or application.properties files). +In the sink example from the Chapter 27, Introducing Spring Cloud Stream section, setting the spring.cloud.stream.bindings.input.destination application property to raw-sensor-data causes it to read from the raw-sensor-data Kafka topic or from a queue bound to the raw-sensor-data RabbitMQ exchange.

    Spring Cloud Stream automatically detects and uses a binder found on the classpath. +You can use different types of middleware with the same code. +To do so, include a different binder at build time. +For more complex use cases, you can also package multiple binders with your application and have it choose the binder( and even whether to use different binders for different channels) at runtime.

    28.3 Persistent Publish-Subscribe Support

    Communication between applications follows a publish-subscribe model, where data is broadcast through shared topics. +This can be seen in the following figure, which shows a typical deployment for a set of interacting Spring Cloud Stream applications.

    Figure 28.2. Spring Cloud Stream Publish-Subscribe

    SCSt sensors

    Data reported by sensors to an HTTP endpoint is sent to a common destination named raw-sensor-data. +From the destination, it is independently processed by a microservice application that computes time-windowed averages and by another microservice application that ingests the raw data into HDFS (Hadoop Distributed File System). +In order to process the data, both applications declare the topic as their input at runtime.

    The publish-subscribe communication model reduces the complexity of both the producer and the consumer and lets new applications be added to the topology without disruption of the existing flow. +For example, downstream from the average-calculating application, you can add an application that calculates the highest temperature values for display and monitoring. +You can then add another application that interprets the same flow of averages for fault detection. +Doing all communication through shared topics rather than point-to-point queues reduces coupling between microservices.

    While the concept of publish-subscribe messaging is not new, Spring Cloud Stream takes the extra step of making it an opinionated choice for its application model. +By using native middleware support, Spring Cloud Stream also simplifies use of the publish-subscribe model across different platforms.

    28.4 Consumer Groups

    While the publish-subscribe model makes it easy to connect applications through shared topics, the ability to scale up by creating multiple instances of a given application is equally important. +When doing so, different instances of an application are placed in a competing consumer relationship, where only one of the instances is expected to handle a given message.

    Spring Cloud Stream models this behavior through the concept of a consumer group. +(Spring Cloud Stream consumer groups are similar to and inspired by Kafka consumer groups.) +Each consumer binding can use the spring.cloud.stream.bindings.<channelName>.group property to specify a group name. +For the consumers shown in the following figure, this property would be set as spring.cloud.stream.bindings.<channelName>.group=hdfsWrite or spring.cloud.stream.bindings.<channelName>.group=average.

    Figure 28.3. Spring Cloud Stream Consumer Groups

    SCSt groups

    All groups that subscribe to a given destination receive a copy of published data, but only one member of each group receives a given message from that destination. +By default, when a group is not specified, Spring Cloud Stream assigns the application to an anonymous and independent single-member consumer group that is in a publish-subscribe relationship with all other consumer groups.

    28.5 Consumer Types

    Two types of consumer are supported:

    • Message-driven (sometimes referred to as Asynchronous)
    • Polled (sometimes referred to as Synchronous)

    Prior to version 2.0, only asynchronous consumers were supported. A message is delivered as soon as it is available and a thread is available to process it.

    When you wish to control the rate at which messages are processed, you might want to use a synchronous consumer.

    28.5.1 Durability

    Consistent with the opinionated application model of Spring Cloud Stream, consumer group subscriptions are durable. +That is, a binder implementation ensures that group subscriptions are persistent and that, once at least one subscription for a group has been created, the group receives messages, even if they are sent while all applications in the group are stopped.

    [Note]Note

    Anonymous subscriptions are non-durable by nature. +For some binder implementations (such as RabbitMQ), it is possible to have non-durable group subscriptions.

    In general, it is preferable to always specify a consumer group when binding an application to a given destination. +When scaling up a Spring Cloud Stream application, you must specify a consumer group for each of its input bindings. +Doing so prevents the application’s instances from receiving duplicate messages (unless that behavior is desired, which is unusual).

    28.6 Partitioning Support

    Spring Cloud Stream provides support for partitioning data between multiple instances of a given application. +In a partitioned scenario, the physical communication medium (such as the broker topic) is viewed as being structured into multiple partitions. +One or more producer application instances send data to multiple consumer application instances and ensure that data identified by common characteristics are processed by the same consumer instance.

    Spring Cloud Stream provides a common abstraction for implementing partitioned processing use cases in a uniform fashion. +Partitioning can thus be used whether the broker itself is naturally partitioned (for example, Kafka) or not (for example, RabbitMQ).

    Figure 28.4. Spring Cloud Stream Partitioning

    SCSt partitioning

    Partitioning is a critical concept in stateful processing, where it is critical (for either performance or consistency reasons) to ensure that all related data is processed together. +For example, in the time-windowed average calculation example, it is important that all measurements from any given sensor are processed by the same application instance.

    [Note]Note

    To set up a partitioned processing scenario, you must configure both the data-producing and the data-consuming ends.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__managing_spans_with_annotations.html b/Greenwich.SR5/multi/multi__managing_spans_with_annotations.html new file mode 100644 index 00000000..5751e701 --- /dev/null +++ b/Greenwich.SR5/multi/multi__managing_spans_with_annotations.html @@ -0,0 +1,45 @@ + + + 60. Managing Spans with Annotations

    60. Managing Spans with Annotations

    You can manage spans with a variety of annotations.

    60.1 Rationale

    There are a number of good reasons to manage spans with annotations, including:

    • API-agnostic means to collaborate with a span. Use of annotations lets users add to a span with no library dependency on a span api. +Doing so lets Sleuth change its core API to create less impact to user code.
    • Reduced surface area for basic span operations. Without this feature, you must use the span api, which has lifecycle commands that could be used incorrectly. +By only exposing scope, tag, and log functionality, you can collaborate without accidentally breaking span lifecycle.
    • Collaboration with runtime generated code. With libraries such as Spring Data and Feign, the implementations of interfaces are generated at runtime. +Consequently, span wrapping of objects was tedious. +Now you can provide annotations over interfaces and the arguments of those interfaces.

    60.2 Creating New Spans

    If you do not want to create local spans manually, you can use the @NewSpan annotation. +Also, we provide the @SpanTag annotation to add tags in an automated fashion.

    Now we can consider some examples of usage.

    @NewSpan
    +void testMethod();

    Annotating the method without any parameter leads to creating a new span whose name equals the annotated method name.

    @NewSpan("customNameOnTestMethod4")
    +void testMethod4();

    If you provide the value in the annotation (either directly or by setting the name parameter), the created span has the provided value as the name.

    // method declaration
    +@NewSpan(name = "customNameOnTestMethod5")
    +void testMethod5(@SpanTag("testTag") String param);
    +
    +// and method execution
    +this.testBean.testMethod5("test");

    You can combine both the name and a tag. Let’s focus on the latter. +In this case, the value of the annotated method’s parameter runtime value becomes the value of the tag. +In our sample, the tag key is testTag, and the tag value is test.

    @NewSpan(name = "customNameOnTestMethod3")
    +@Override
    +public void testMethod3() {
    +}

    You can place the @NewSpan annotation on both the class and an interface. +If you override the interface’s method and provide a different value for the @NewSpan annotation, the most +concrete one wins (in this case customNameOnTestMethod3 is set).

    60.3 Continuing Spans

    If you want to add tags and annotations to an existing span, you can use the @ContinueSpan annotation, as shown in the following example:

    // method declaration
    +@ContinueSpan(log = "testMethod11")
    +void testMethod11(@SpanTag("testTag11") String param);
    +
    +// method execution
    +this.testBean.testMethod11("test");
    +this.testBean.testMethod13();

    (Note that, in contrast with the @NewSpan annotation ,you can also add logs with the log parameter.)

    That way, the span gets continued and:

    • Log entries named testMethod11.before and testMethod11.after are created.
    • If an exception is thrown, a log entry named testMethod11.afterFailure is also created.
    • A tag with a key of testTag11 and a value of test is created.

    60.4 Advanced Tag Setting

    There are 3 different ways to add tags to a span. All of them are controlled by the SpanTag annotation. +The precedence is as follows:

    1. Try with a bean of TagValueResolver type and a provided name.
    2. If the bean name has not been provided, try to evaluate an expression. +We search for a TagValueExpressionResolver bean. +The default implementation uses SPEL expression resolution. +IMPORTANT You can only reference properties from the SPEL expression. Method execution is not allowed due to security constraints.
    3. If we do not find any expression to evaluate, return the toString() value of the parameter.

    60.4.1 Custom extractor

    The value of the tag for the following method is computed by an implementation of TagValueResolver interface. +Its class name has to be passed as the value of the resolver attribute.

    Consider the following annotated method:

    @NewSpan
    +public void getAnnotationForTagValueResolver(
    +		@SpanTag(key = "test", resolver = TagValueResolver.class) String test) {
    +}

    Now further consider the following TagValueResolver bean implementation:

    @Bean(name = "myCustomTagValueResolver")
    +public TagValueResolver tagValueResolver() {
    +	return parameter -> "Value from myCustomTagValueResolver";
    +}

    The two preceding examples lead to setting a tag value equal to Value from myCustomTagValueResolver.

    60.4.2 Resolving Expressions for a Value

    Consider the following annotated method:

    @NewSpan
    +public void getAnnotationForTagValueExpression(
    +		@SpanTag(key = "test", expression = "'hello' + ' characters'") String test) {
    +}

    No custom implementation of a TagValueExpressionResolver leads to evaluation of the SPEL expression, and a tag with a value of 4 characters is set on the span. +If you want to use some other expression resolution mechanism, you can create your own implementation of the bean.

    60.4.3 Using the toString() method

    Consider the following annotated method:

    @NewSpan
    +public void getAnnotationForArgumentToString(@SpanTag("test") Long param) {
    +}

    Running the preceding method with a value of 15 leads to setting a tag with a String value of "15".

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__migrations.html b/Greenwich.SR5/multi/multi__migrations.html new file mode 100644 index 00000000..da943525 --- /dev/null +++ b/Greenwich.SR5/multi/multi__migrations.html @@ -0,0 +1,96 @@ + + + 98. Migrations

    98. Migrations

    [Tip]Tip

    For up to date migration guides please visit +the project’s wiki page.

    This section covers migrating from one version of Spring Cloud Contract Verifier to the +next version. It covers the following versions upgrade paths:

    98.1 1.0.x → 1.1.x

    This section covers upgrading from version 1.0 to version 1.1.

    98.1.1 New structure of generated stubs

    In 1.1.x we have introduced a change to the structure of generated stubs. If you have +been using the @AutoConfigureWireMock notation to use the stubs from the classpath, +it no longer works. The following example shows how the @AutoConfigureWireMock notation +used to work:

    @AutoConfigureWireMock(stubs = "classpath:/customer-stubs/mappings", port = 8084)

    You must either change the location of the stubs to: +classpath:…​/META-INF/groupId/artifactId/version/mappings or use the new +classpath-based @AutoConfigureStubRunner, as shown in the following example:

    @AutoConfigureWireMock(stubs = "classpath:customer-stubs/META-INF/travel.components/customer-contract/1.0.2-SNAPSHOT/mappings/", port = 8084)

    If you do not want to use @AutoConfigureStubRunner and you want to remain with the old +structure, set your plugin tasks accordingly. The following example would work for the +structure presented in the previous snippet.

    Maven.  +

    <!-- start of pom.xml -->
    +
    +<properties>
    +    <!-- we don't want the verifier to do a jar for us -->
    +    <spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>
    +</properties>
    +
    +<!-- ... -->
    +
    +<!-- You need to set up the assembly plugin -->
    +<build>
    +    <plugins>
    +        <plugin>
    +            <groupId>org.apache.maven.plugins</groupId>
    +            <artifactId>maven-assembly-plugin</artifactId>
    +            <executions>
    +                <execution>
    +                    <id>stub</id>
    +                    <phase>prepare-package</phase>
    +                    <goals>
    +                        <goal>single</goal>
    +                    </goals>
    +                    <inherited>false</inherited>
    +                    <configuration>
    +                        <attach>true</attach>
    +                        <descriptor>$../../../../src/assembly/stub.xml</descriptor>
    +                    </configuration>
    +                </execution>
    +            </executions>
    +        </plugin>
    +    </plugins>
    +</build>
    +<!-- end of pom.xml -->
    +
    +<!-- start of stub.xml-->
    +
    +<assembly
    +	xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    +	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
    +	<id>stubs</id>
    +	<formats>
    +		<format>jar</format>
    +	</formats>
    +	<includeBaseDirectory>false</includeBaseDirectory>
    +	<fileSets>
    +		<fileSet>
    +			<directory>${project.build.directory}/snippets/stubs</directory>
    +			<outputDirectory>customer-stubs/mappings</outputDirectory>
    +			<includes>
    +				<include>**/*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>$../../../../src/test/resources/contracts</directory>
    +			<outputDirectory>customer-stubs/contracts</outputDirectory>
    +			<includes>
    +				<include>**/*.groovy</include>
    +			</includes>
    +		</fileSet>
    +	</fileSets>
    +</assembly>
    +
    +<!-- end of stub.xml-->

    +

    Gradle.  +

    task copyStubs(type: Copy, dependsOn: 'generateWireMockClientStubs') {
    +//    Preserve directory structure from 1.0.X of spring-cloud-contract
    +    from "${project.buildDir}/resources/main/customer-stubs/META-INF/${project.group}/${project.name}/${project.version}"
    +    into "${project.buildDir}/resources/main/customer-stubs"
    +}

    +

    98.2 1.1.x → 1.2.x

    This section covers upgrading from version 1.1 to version 1.2.

    98.2.1 Custom HttpServerStub

    HttpServerStub includes a method that was not in version 1.1. The method is +String registeredMappings() If you have classes that implement HttpServerStub, you +now have to implement the registeredMappings() method. It should return a String +representing all mappings available in a single HttpServerStub.

    See issue 355 for more +detail.

    98.2.2 New packages for generated tests

    The flow for setting the generated tests package name will look like this:

    • Set basePackageForTests
    • If basePackageForTests was not set, pick the package from baseClassForTests
    • If baseClassForTests was not set, pick packageWithBaseClasses
    • If nothing got set, pick the default value: +org.springframework.cloud.contract.verifier.tests

    See issue 260 for more +detail.

    98.2.3 New Methods in TemplateProcessor

    In order to add support for fromRequest.path, the following methods had to be added to the +TemplateProcessor interface:

    • path()
    • path(int index)

    See issue 388 for more +detail.

    98.2.4 RestAssured 3.0

    Rest Assured, used in the generated test classes, got bumped to 3.0. If +you manually set versions of Spring Cloud Contract and the release train +you might see the following exception:

    Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile (default-testCompile) on project some-project: Compilation failure: Compilation failure:
    +[ERROR] /some/path/SomeClass.java:[4,39] package com.jayway.restassured.response does not exist

    This exception will occur due to the fact that the tests got generated with +an old version of plugin and at test execution time you have an incompatible +version of the release train (and vice versa).

    Done via issue 267

    98.3 1.2.x → 2.0.x

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__modules_in_maintenance_mode.html b/Greenwich.SR5/multi/multi__modules_in_maintenance_mode.html new file mode 100644 index 00000000..25dc6958 --- /dev/null +++ b/Greenwich.SR5/multi/multi__modules_in_maintenance_mode.html @@ -0,0 +1,5 @@ + + + 22. Modules In Maintenance Mode

    22. Modules In Maintenance Mode

    Placing a module in maintenance mode means that the Spring Cloud team will no longer be adding new features to the module. +We will fix blocker bugs and security issues, and we will also consider and review small pull requests from the community.

    We intend to continue to support these modules for a period of at least a year from the general availability +of the Greenwich release train.

    The following Spring Cloud Netflix modules and corresponding starters will be placed into maintenance mode:

    • spring-cloud-netflix-archaius
    • spring-cloud-netflix-hystrix-contract
    • spring-cloud-netflix-hystrix-dashboard
    • spring-cloud-netflix-hystrix-stream
    • spring-cloud-netflix-hystrix
    • spring-cloud-netflix-ribbon
    • spring-cloud-netflix-turbine-stream
    • spring-cloud-netflix-turbine
    • spring-cloud-netflix-zuul
    [Note]Note

    This does not include the Eureka or concurrency-limits modules.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__more_detail.html b/Greenwich.SR5/multi/multi__more_detail.html new file mode 100644 index 00000000..632c6ae7 --- /dev/null +++ b/Greenwich.SR5/multi/multi__more_detail.html @@ -0,0 +1,120 @@ + + + 82. More Detail

    82. More Detail

    82.1 Single Sign On

    [Note]Note

    All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +Spring Boot user guide.

    82.2 Token Relay

    A Token Relay is where an OAuth2 consumer acts as a Client and +forwards the incoming token to outgoing resource requests. The +consumer can be a pure Client (like an SSO application) or a Resource +Server.

    82.2.1 Client Token Relay in Spring Cloud Gateway

    If your app also has a +Spring +Cloud Gateway embedded reverse proxy then you +can ask it to forward OAuth2 access tokens downstream to the services +it is proxying. Thus the SSO app above can be enhanced simply like +this:

    App.java.  +

    @Autowired
    +private TokenRelayGatewayFilterFactory filterFactory;
    +
    +@Bean
    +public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    +    return builder.routes()
    +            .route("resource", r -> r.path("/resource")
    +                    .filters(f -> f.filter(filterFactory.apply()))
    +                    .uri("http://localhost:9000"))
    +            .build();
    +}

    +

    or this

    application.yaml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: resource
    +        uri: http://localhost:9000
    +        predicates:
    +        - Path=/resource
    +        filters:
    +        - TokenRelay=

    +

    and it will (in addition to logging the user in and grabbing a token) +pass the authentication token downstream to the services (in this case +/resource).

    To enable this for Spring Cloud Gateway add the following dependencies

    • org.springframework.boot:spring-boot-starter-oauth2-client
    • org.springframework.cloud:spring-cloud-starter-security

    How does it work? The +filter +extracts an access token from the currently authenticated user, +and puts it in a request header for the downstream requests.

    For a full working sample see this project.

    [Note]Note

    The default implementation of ReactiveOAuth2AuthorizedClientService used by TokenRelayGatewayFilterFactory +uses an in-memory data store. You will need to provide your own implementation ReactiveOAuth2AuthorizedClientService +if you need a more robust solution.

    82.2.2 Client Token Relay

    If your app is a user facing OAuth2 client (i.e. has declared +@EnableOAuth2Sso or @EnableOAuth2Client) then it has an +OAuth2ClientContext in request scope from Spring Boot. You can +create your own OAuth2RestTemplate from this context and an +autowired OAuth2ProtectedResourceDetails, and then the context will +always forward the access token downstream, also refreshing the access +token automatically if it expires. (These are features of Spring +Security and Spring Boot.)

    [Note]Note

    Spring Boot (1.4.1) does not create an +OAuth2ProtectedResourceDetails automatically if you are using +client_credentials tokens. In that case you need to create your own +ClientCredentialsResourceDetails and configure it with +@ConfigurationProperties("security.oauth2.client").

    82.2.3 Client Token Relay in Zuul Proxy

    If your app also has a +Spring +Cloud Zuul embedded reverse proxy (using @EnableZuulProxy) then you +can ask it to forward OAuth2 access tokens downstream to the services +it is proxying. Thus the SSO app above can be enhanced simply like +this:

    app.groovy.  +

    @Controller
    +@EnableOAuth2Sso
    +@EnableZuulProxy
    +class Application {
    +
    +}

    +

    and it will (in addition to logging the user in and grabbing a token) +pass the authentication token downstream to the /proxy/* +services. If those services are implemented with +@EnableResourceServer then they will get a valid token in the +correct header.

    How does it work? The @EnableOAuth2Sso annotation pulls in +spring-cloud-starter-security (which you could do manually in a +traditional app), and that in turn triggers some autoconfiguration for +a ZuulFilter, which itself is activated because Zuul is on the +classpath (via @EnableZuulProxy). The +filter +just extracts an access token from the currently authenticated user, +and puts it in a request header for the downstream requests.

    [Note]Note

    Spring Boot does not create an OAuth2RestOperations automatically which is needed for refresh_token. In that case you need to create your own +OAuth2RestOperations so OAuth2TokenRelayFilter can refresh the token if needed.

    82.2.4 Resource Server Token Relay

    If your app has @EnableResourceServer you might want to relay the +incoming token downstream to other services. If you use a +RestTemplate to contact the downstream services then this is just a +matter of how to create the template with the right context.

    If your service uses UserInfoTokenServices to authenticate incoming +tokens (i.e. it is using the security.oauth2.user-info-uri +configuration), then you can simply create an OAuth2RestTemplate +using an autowired OAuth2ClientContext (it will be populated by the +authentication process before it hits the backend code). Equivalently +(with Spring Boot 1.4), you could inject a +UserInfoRestTemplateFactory and grab its OAuth2RestTemplate in +your configuration. For example:

    MyConfiguration.java.  +

    @Bean
    +public OAuth2RestTemplate restTemplate(UserInfoRestTemplateFactory factory) {
    +    return factory.getUserInfoRestTemplate();
    +}

    +

    This rest template will then have the same OAuth2ClientContext +(request-scoped) that is used by the authentication filter, so you can +use it to send requests with the same access token.

    If your app is not using UserInfoTokenServices but is still a client +(i.e. it declares @EnableOAuth2Client or @EnableOAuth2Sso), then +with Spring Security Cloud any OAuth2RestOperations that the user +creates from an @Autowired OAuth2Context will also forward +tokens. This feature is implemented by default as an MVC handler +interceptor, so it only works in Spring MVC. If you are not using MVC +you could use a custom filter or AOP interceptor wrapping an +AccessTokenContextRelay to provide the same feature.

    Here’s a basic +example showing the use of an autowired rest template created +elsewhere ("foo.com" is a Resource Server accepting the same tokens as +the surrounding app):

    MyController.java.  +

    @Autowired
    +private OAuth2RestOperations restTemplate;
    +
    +@RequestMapping("/relay")
    +public String relay() {
    +    ResponseEntity<String> response =
    +      restTemplate.getForEntity("https://foo.com/bar", String.class);
    +    return "Success! (" + response.getBody() + ")";
    +}

    +

    If you don’t want to forward tokens (and that is a valid +choice, since you might want to act as yourself, rather than the +client that sent you the token), then you only need to create your own +OAuth2Context instead of autowiring the default one.

    Feign clients will also pick up an interceptor that uses the +OAuth2ClientContext if it is available, so they should also do a +token relay anywhere where a RestTemplate would.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__naming_spans.html b/Greenwich.SR5/multi/multi__naming_spans.html new file mode 100644 index 00000000..30dc5dca --- /dev/null +++ b/Greenwich.SR5/multi/multi__naming_spans.html @@ -0,0 +1,34 @@ + + + 59. Naming spans

    59. Naming spans

    Picking a span name is not a trivial task. A span name should depict an operation name. +The name should be low cardinality, so it should not include identifiers.

    Since there is a lot of instrumentation going on, some span names are artificial:

    • controller-method-name when received by a Controller with a method name of controllerMethodName
    • async for asynchronous operations done with wrapped Callable and Runnable interfaces.
    • Methods annotated with @Scheduled return the simple name of the class.

    Fortunately, for asynchronous processing, you can provide explicit naming.

    59.1 @SpanName Annotation

    You can name the span explicitly by using the @SpanName annotation, as shown in the following example:

    	@SpanName("calculateTax")
    +	class TaxCountingRunnable implements Runnable {
    +
    +		@Override
    +		public void run() {
    +			// perform logic
    +		}
    +
    +	}
    +
    +}

    In this case, when processed in the following manner, the span is named calculateTax:

    Runnable runnable = new TraceRunnable(this.tracing, spanNamer,
    +		new TaxCountingRunnable());
    +Future<?> future = executorService.submit(runnable);
    +// ... some additional logic ...
    +future.get();

    59.2 toString() method

    It is pretty rare to create separate classes for Runnable or Callable. +Typically, one creates an anonymous instance of those classes. +You cannot annotate such classes. +To overcome that limitation, if there is no @SpanName annotation present, we check whether the class has a custom implementation of the toString() method.

    Running such code leads to creating a span named calculateTax, as shown in the following example:

    Runnable runnable = new TraceRunnable(this.tracing, spanNamer, new Runnable() {
    +	@Override
    +	public void run() {
    +		// perform logic
    +	}
    +
    +	@Override
    +	public String toString() {
    +		return "calculateTax";
    +	}
    +});
    +Future<?> future = executorService.submit(runnable);
    +// ... some additional logic ...
    +future.get();
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__other_resources.html b/Greenwich.SR5/multi/multi__other_resources.html new file mode 100644 index 00000000..4c43e844 --- /dev/null +++ b/Greenwich.SR5/multi/multi__other_resources.html @@ -0,0 +1,3 @@ + + + 148. Other Resources

    148. Other Resources

    This section lists other resources, such as presentations (slides) and videos about Spring Cloud Kubernetes.

    Please feel free to submit other resources through pull requests to this repository.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__pod_health_indicator.html b/Greenwich.SR5/multi/multi__pod_health_indicator.html new file mode 100644 index 00000000..3d4be42e --- /dev/null +++ b/Greenwich.SR5/multi/multi__pod_health_indicator.html @@ -0,0 +1,4 @@ + + + 143. Pod Health Indicator

    143. Pod Health Indicator

    Spring Boot uses HealthIndicator to expose info about the health of an application. +That makes it really useful for exposing health-related information to the user and makes it a good fit for use as readiness probes.

    The Kubernetes health indicator (which is part of the core module) exposes the following info:

    • Pod name, IP address, namespace, service account, node name, and its IP address
    • A flag that indicates whether the Spring Boot application is internal or external to Kubernetes
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__polyglot_support_with_sidecar.html b/Greenwich.SR5/multi/multi__polyglot_support_with_sidecar.html new file mode 100644 index 00000000..293c693b --- /dev/null +++ b/Greenwich.SR5/multi/multi__polyglot_support_with_sidecar.html @@ -0,0 +1,59 @@ + + + 19. Polyglot support with Sidecar

    19. Polyglot support with Sidecar

    Do you have non-JVM languages with which you want to take advantage of Eureka, Ribbon, and Config Server? +The Spring Cloud Netflix Sidecar was inspired by Netflix Prana. +It includes an HTTP API to get all of the instances (by host and port) for a given service. +You can also proxy service calls through an embedded Zuul proxy that gets its route entries from Eureka. +The Spring Cloud Config Server can be accessed directly through host lookup or through the Zuul Proxy. +The non-JVM application should implement a health check so the Sidecar can report to Eureka whether the app is up or down.

    To include Sidecar in your project, use the dependency with a group ID of org.springframework.cloud +and artifact ID or spring-cloud-netflix-sidecar.

    To enable the Sidecar, create a Spring Boot application with @EnableSidecar. +This annotation includes @EnableCircuitBreaker, @EnableDiscoveryClient, and @EnableZuulProxy. +Run the resulting application on the same host as the non-JVM application.

    To configure the side car, add sidecar.port and sidecar.health-uri to application.yml. +The sidecar.port property is the port on which the non-JVM application listens. +This is so the Sidecar can properly register the application with Eureka. +The sidecar.secure-port-enabled options provides a way to enable secure port for traffic. +The sidecar.health-uri is a URI accessible on the non-JVM application that mimics a Spring Boot health indicator. +It should return a JSON document that resembles the following:

    health-uri-document.  +

    {
    +  "status":"UP"
    +}

    +

    The following application.yml example shows sample configuration for a Sidecar application:

    application.yml.  +

    server:
    +  port: 5678
    +spring:
    +  application:
    +    name: sidecar
    +
    +sidecar:
    +  port: 8000
    +  health-uri: http://localhost:8000/health.json

    +

    The API for the DiscoveryClient.getInstances() method is /hosts/{serviceId}. +The following example response for /hosts/customers returns two instances on different hosts:

    /hosts/customers.  +

    [
    +    {
    +        "host": "myhost",
    +        "port": 9000,
    +        "uri": "http://myhost:9000",
    +        "serviceId": "CUSTOMERS",
    +        "secure": false
    +    },
    +    {
    +        "host": "myhost2",
    +        "port": 9000,
    +        "uri": "http://myhost2:9000",
    +        "serviceId": "CUSTOMERS",
    +        "secure": false
    +    }
    +]

    +

    This API is accessible to the non-JVM application (if the sidecar is on port 5678) at http://localhost:5678/hosts/{serviceId}.

    The Zuul proxy automatically adds routes for each service known in Eureka to /<serviceId>, so the customers service is available at /customers. +The non-JVM application can access the customer service at http://localhost:5678/customers (assuming the sidecar is listening on port 5678).

    If the Config Server is registered with Eureka, the non-JVM application can access it through the Zuul proxy. +If the serviceId of the ConfigServer is configserver and the Sidecar is on port 5678, then it can be accessed at http://localhost:5678/configserver.

    Non-JVM applications can take advantage of the Config Server’s ability to return YAML documents. +For example, a call to https://sidecar.local.spring.io:5678/configserver/default-master.yml +might result in a YAML document resembling the following:

    eureka:
    +  client:
    +    serviceUrl:
    +      defaultZone: http://localhost:8761/eureka/
    +  password: password
    +info:
    +  description: Spring Cloud Samples
    +  url: https://github.com/spring-cloud-samples

    To enable the health check request to accept all certificates when using HTTPs set sidecar.accept-all-ssl-certificates to `true.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__programming_model.html b/Greenwich.SR5/multi/multi__programming_model.html new file mode 100644 index 00000000..1fa65e8f --- /dev/null +++ b/Greenwich.SR5/multi/multi__programming_model.html @@ -0,0 +1,395 @@ + + + 29. Programming Model

    29. Programming Model

    To understand the programming model, you should be familiar with the following core concepts:

    • Destination Binders: Components responsible to provide integration with the external messaging systems.
    • Destination Bindings: Bridge between the external messaging systems and application provided Producers and Consumers of messages (created by the Destination Binders).
    • Message: The canonical data structure used by producers and consumers to communicate with Destination Binders (and thus other applications via external messaging systems).
    SCSt overview

    29.1 Destination Binders

    Destination Binders are extension components of Spring Cloud Stream responsible for providing the necessary configuration and implementation to facilitate +integration with external messaging systems. +This integration is responsible for connectivity, delegation, and routing of messages to and from producers and consumers, data type conversion, +invocation of the user code, and more.

    Binders handle a lot of the boiler plate responsibilities that would otherwise fall on your shoulders. However, to accomplish that, the binder still needs +some help in the form of minimalistic yet required set of instructions from the user, which typically come in the form of some type of configuration.

    While it is out of scope of this section to discuss all of the available binder and binding configuration options (the rest of the manual covers them extensively), +Destination Binding does require special attention. The next section discusses it in detail.

    29.2 Destination Bindings

    As stated earlier, Destination Bindings provide a bridge between the external messaging system and application-provided Producers and Consumers.

    Applying the @EnableBinding annotation to one of the application’s configuration classes defines a destination binding. +The @EnableBinding annotation itself is meta-annotated with @Configuration and triggers the configuration of the Spring Cloud Stream infrastructure.

    The following example shows a fully configured and functioning Spring Cloud Stream application that receives the payload of the message from the INPUT +destination as a String type (see Chapter 32, Content Type Negotiation section), logs it to the console and sends it to the OUTPUT destination after converting it to upper case.

    @SpringBootApplication
    +@EnableBinding(Processor.class)
    +public class MyApplication {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(MyApplication.class, args);
    +	}
    +
    +	@StreamListener(Processor.INPUT)
    +	@SendTo(Processor.OUTPUT)
    +	public String handle(String value) {
    +		System.out.println("Received: " + value);
    +		return value.toUpperCase();
    +	}
    +}

    As you can see the @EnableBinding annotation can take one or more interface classes as parameters. The parameters are referred to as bindings, +and they contain methods representing bindable components. +These components are typically message channels (see Spring Messaging) +for channel-based binders (such as Rabbit, Kafka, and others). However other types of bindings can +provide support for the native features of the corresponding technology. For example Kafka Streams binder (formerly known as KStream) allows native bindings directly to Kafka Streams +(see Kafka Streams for more details).

    Spring Cloud Stream already provides binding interfaces for typical message exchange contracts, which include:

    • Sink: Identifies the contract for the message consumer by providing the destination from which the message is consumed.
    • Source: Identifies the contract for the message producer by providing the destination to which the produced message is sent.
    • Processor: Encapsulates both the sink and the source contracts by exposing two destinations that allow consumption and production of messages.
    public interface Sink {
    +
    +  String INPUT = "input";
    +
    +  @Input(Sink.INPUT)
    +  SubscribableChannel input();
    +}
    public interface Source {
    +
    +  String OUTPUT = "output";
    +
    +  @Output(Source.OUTPUT)
    +  MessageChannel output();
    +}
    public interface Processor extends Source, Sink {}

    While the preceding example satisfies the majority of cases, you can also define your own contracts by defining your own bindings interfaces and use @Input and @Output +annotations to identify the actual bindable components.

    For example:

    public interface Barista {
    +
    +    @Input
    +    SubscribableChannel orders();
    +
    +    @Output
    +    MessageChannel hotDrinks();
    +
    +    @Output
    +    MessageChannel coldDrinks();
    +}

    Using the interface shown in the preceding example as a parameter to @EnableBinding triggers the creation of the three bound channels named orders, hotDrinks, and coldDrinks, +respectively.

    You can provide as many binding interfaces as you need, as arguments to the @EnableBinding annotation, as shown in the following example:

    @EnableBinding(value = { Orders.class, Payment.class })

    In Spring Cloud Stream, the bindable MessageChannel components are the Spring Messaging MessageChannel (for outbound) and its extension, SubscribableChannel, +(for inbound).

    Pollable Destination Binding

    While the previously described bindings support event-based message consumption, sometimes you need more control, such as rate of consumption.

    Starting with version 2.0, you can now bind a pollable consumer:

    The following example shows how to bind a pollable consumer:

    public interface PolledBarista {
    +
    +    @Input
    +    PollableMessageSource orders();
    +	. . .
    +}

    In this case, an implementation of PollableMessageSource is bound to the orders “channel”. See Section 29.3.5, “Using Polled Consumers” for more details.

    Customizing Channel Names

    By using the @Input and @Output annotations, you can specify a customized channel name for the channel, as shown in the following example:

    public interface Barista {
    +    @Input("inboundOrders")
    +    SubscribableChannel orders();
    +}

    In the preceding example, the created bound channel is named inboundOrders.

    Normally, you need not access individual channels or bindings directly (other then configuring them via @EnableBinding annotation). However there may be +times, such as testing or other corner cases, when you do.

    Aside from generating channels for each binding and registering them as Spring beans, for each bound interface, Spring Cloud Stream generates a bean that implements the interface. +That means you can have access to the interfaces representing the bindings or individual channels by auto-wiring either in your application, as shown in the following two examples:

    Autowire Binding interface

    @Autowire
    +private Source source
    +
    +public void sayHello(String name) {
    +    source.output().send(MessageBuilder.withPayload(name).build());
    +}

    Autowire individual channel

    @Autowire
    +private MessageChannel output;
    +
    +public void sayHello(String name) {
    +    output.send(MessageBuilder.withPayload(name).build());
    +}

    You can also use standard Spring’s @Qualifier annotation for cases when channel names are customized or in multiple-channel scenarios that require specifically named channels.

    The following example shows how to use the @Qualifier annotation in this way:

    @Autowire
    +@Qualifier("myChannel")
    +private MessageChannel output;

    29.3 Producing and Consuming Messages

    You can write a Spring Cloud Stream application by using either Spring Integration annotations or Spring Cloud Stream native annotation.

    29.3.1 Spring Integration Support

    Spring Cloud Stream is built on the concepts and patterns defined by Enterprise Integration Patterns and relies +in its internal implementation on an already established and popular implementation of Enterprise Integration Patterns within the Spring portfolio of projects: +Spring Integration framework.

    So its only natural for it to support the foundation, semantics, and configuration options that are already established by Spring Integration

    For example, you can attach the output channel of a Source to a MessageSource and use the familiar @InboundChannelAdapter annotation, as follows:

    @EnableBinding(Source.class)
    +public class TimerSource {
    +
    +  @Bean
    +  @InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedDelay = "10", maxMessagesPerPoll = "1"))
    +  public MessageSource<String> timerMessageSource() {
    +    return () -> new GenericMessage<>("Hello Spring Cloud Stream");
    +  }
    +}

    Similarly, you can use @Transformer or @ServiceActivator while providing an implementation of a message handler method for a Processor binding contract, as shown in the following example:

    @EnableBinding(Processor.class)
    +public class TransformProcessor {
    +  @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
    +  public Object transform(String message) {
    +    return message.toUpperCase();
    +  }
    +}
    [Note]Note

    While this may be skipping ahead a bit, it is important to understand that, when you consume from the same binding using @StreamListener annotation, a pub-sub model is used. +Each method annotated with @StreamListener receives its own copy of a message, and each one has its own consumer group. +However, if you consume from the same binding by using one of the Spring Integration annotation (such as @Aggregator, @Transformer, or @ServiceActivator), those consume in a competing model. +No individual consumer group is created for each subscription.

    29.3.2 Using @StreamListener Annotation

    Complementary to its Spring Integration support, Spring Cloud Stream provides its own @StreamListener annotation, modeled after other Spring Messaging annotations +(@MessageMapping, @JmsListener, @RabbitListener, and others) and provides conviniences, such as content-based routing and others.

    @EnableBinding(Sink.class)
    +public class VoteHandler {
    +
    +  @Autowired
    +  VotingService votingService;
    +
    +  @StreamListener(Sink.INPUT)
    +  public void handle(Vote vote) {
    +    votingService.record(vote);
    +  }
    +}

    As with other Spring Messaging methods, method arguments can be annotated with @Payload, @Headers, and @Header.

    For methods that return data, you must use the @SendTo annotation to specify the output binding destination for data returned by the method, as shown in the following example:

    @EnableBinding(Processor.class)
    +public class TransformProcessor {
    +
    +  @Autowired
    +  VotingService votingService;
    +
    +  @StreamListener(Processor.INPUT)
    +  @SendTo(Processor.OUTPUT)
    +  public VoteResult handle(Vote vote) {
    +    return votingService.record(vote);
    +  }
    +}

    29.3.3 Using @StreamListener for Content-based routing

    Spring Cloud Stream supports dispatching messages to multiple handler methods annotated with @StreamListener based on conditions.

    In order to be eligible to support conditional dispatching, a method must satisfy the follow conditions:

    • It must not return a value.
    • It must be an individual message handling method (reactive API methods are not supported).

    The condition is specified by a SpEL expression in the condition argument of the annotation and is evaluated for each message. +All the handlers that match the condition are invoked in the same thread, and no assumption must be made about the order in which the invocations take place.

    In the following example of a @StreamListener with dispatching conditions, all the messages bearing a header type with the value bogey are dispatched to the +receiveBogey method, and all the messages bearing a header type with the value bacall are dispatched to the receiveBacall method.

    @EnableBinding(Sink.class)
    +@EnableAutoConfiguration
    +public static class TestPojoWithAnnotatedArguments {
    +
    +    @StreamListener(target = Sink.INPUT, condition = "headers['type']=='bogey'")
    +    public void receiveBogey(@Payload BogeyPojo bogeyPojo) {
    +       // handle the message
    +    }
    +
    +    @StreamListener(target = Sink.INPUT, condition = "headers['type']=='bacall'")
    +    public void receiveBacall(@Payload BacallPojo bacallPojo) {
    +       // handle the message
    +    }
    +}

    Content Type Negotiation in the Context of condition

    It is important to understand some of the mechanics behind content-based routing using the condition argument of @StreamListener, especially in the context of the type of the message as a whole. +It may also help if you familiarize yourself with the Chapter 32, Content Type Negotiation before you proceed.

    Consider the following scenario:

    @EnableBinding(Sink.class)
    +@EnableAutoConfiguration
    +public static class CatsAndDogs {
    +
    +    @StreamListener(target = Sink.INPUT, condition = "payload.class.simpleName=='Dog'")
    +    public void bark(Dog dog) {
    +       // handle the message
    +    }
    +
    +    @StreamListener(target = Sink.INPUT, condition = "payload.class.simpleName=='Cat'")
    +    public void purr(Cat cat) {
    +       // handle the message
    +    }
    +}

    The preceding code is perfectly valid. It compiles and deploys without any issues, yet it never produces the result you expect.

    That is because you are testing something that does not yet exist in a state you expect. That is because the payload of the message is not yet converted from the +wire format (byte[]) to the desired type. +In other words, it has not yet gone through the type conversion process described in the Chapter 32, Content Type Negotiation.

    So, unless you use a SPeL expression that evaluates raw data (for example, the value of the first byte in the byte array), use message header-based expressions +(such as condition = "headers['type']=='dog'").

    [Note]Note

    At the moment, dispatching through @StreamListener conditions is supported only for channel-based binders (not for reactive programming) +support.

    29.3.4 Spring Cloud Function support

    Since Spring Cloud Stream v2.1, another alternative for defining stream handlers and sources is to use build-in +support for Spring Cloud Function where they can be expressed as beans of + type java.util.function.[Supplier/Function/Consumer].

    To specify which functional bean to bind to the external destination(s) exposed by the bindings, you must provide spring.cloud.stream.function.definition property.

    Here is the example of the Processor application exposing message handler as java.util.function.Function

    @SpringBootApplication
    +@EnableBinding(Processor.class)
    +public class MyFunctionBootApp {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(MyFunctionBootApp.class, "--spring.cloud.stream.function.definition=toUpperCase");
    +	}
    +
    +	@Bean
    +	public Function<String, String> toUpperCase() {
    +		return s -> s.toUpperCase();
    +	}
    +}

    In the above you we simply define a bean of type java.util.function.Function called toUpperCase and identify it as a bean to be used as message handler +whose 'input' and 'output' must be bound to the external destinations exposed by the Processor binding.

    Below are the examples of simple functional applications to support Source, Processor and Sink.

    Here is the example of a Source application defined as java.util.function.Supplier

    @SpringBootApplication
    +@EnableBinding(Source.class)
    +public static class SourceFromSupplier {
    +	public static void main(String[] args) {
    +		SpringApplication.run(SourceFromSupplier.class, "--spring.cloud.stream.function.definition=date");
    +	}
    +	@Bean
    +	public Supplier<Date> date() {
    +		return () -> new Date(12345L);
    +	}
    +}

    Here is the example of a Processor application defined as java.util.function.Function

    @SpringBootApplication
    +@EnableBinding(Processor.class)
    +public static class ProcessorFromFunction {
    +	public static void main(String[] args) {
    +		SpringApplication.run(ProcessorFromFunction.class, "--spring.cloud.stream.function.definition=toUpperCase");
    +	}
    +	@Bean
    +	public Function<String, String> toUpperCase() {
    +		return s -> s.toUpperCase();
    +	}
    +}

    Here is the example of a Sink application defined as java.util.function.Consumer

    @EnableAutoConfiguration
    +@EnableBinding(Sink.class)
    +public static class SinkFromConsumer {
    +	public static void main(String[] args) {
    +		SpringApplication.run(SinkFromConsumer.class, "--spring.cloud.stream.function.definition=sink");
    +	}
    +	@Bean
    +	public Consumer<String> sink() {
    +		return System.out::println;
    +	}
    +}

    Functional Composition

    Using this programming model you can also benefit from functional composition where you can dynamically compose complex handlers from a set of simple functions. +As an example let’s add the following function bean to the application defined above

    @Bean
    +public Function<String, String> wrapInQuotes() {
    +	return s -> "\"" + s + "\"";
    +}

    and modify the spring.cloud.stream.function.definition property to reflect your intention to compose a new function from both ‘toUpperCase’ and ‘wrapInQuotes’. +To do that Spring Cloud Function allows you to use | (pipe) symbol. So to finish our example our property will now look like this:

    —spring.cloud.stream.function.definition=toUpperCase|wrapInQuotes

    29.3.5 Using Polled Consumers

    Overview

    When using polled consumers, you poll the PollableMessageSource on demand. +Consider the following example of a polled consumer:

    public interface PolledConsumer {
    +
    +    @Input
    +    PollableMessageSource destIn();
    +
    +    @Output
    +    MessageChannel destOut();
    +
    +}

    Given the polled consumer in the preceding example, you might use it as follows:

    @Bean
    +public ApplicationRunner poller(PollableMessageSource destIn, MessageChannel destOut) {
    +    return args -> {
    +        while (someCondition()) {
    +            try {
    +                if (!destIn.poll(m -> {
    +                    String newPayload = ((String) m.getPayload()).toUpperCase();
    +                    destOut.send(new GenericMessage<>(newPayload));
    +                })) {
    +                    Thread.sleep(1000);
    +                }
    +            }
    +            catch (Exception e) {
    +                // handle failure
    +            }
    +        }
    +    };
    +}

    The PollableMessageSource.poll() method takes a MessageHandler argument (often a lambda expression, as shown here). +It returns true if the message was received and successfully processed.

    As with message-driven consumers, if the MessageHandler throws an exception, messages are published to error channels, as discussed in ???.

    Normally, the poll() method acknowledges the message when the MessageHandler exits. +If the method exits abnormally, the message is rejected (not re-queued), but see the section called “Handling Errors”. +You can override that behavior by taking responsibility for the acknowledgment, as shown in the following example:

    @Bean
    +public ApplicationRunner poller(PollableMessageSource dest1In, MessageChannel dest2Out) {
    +    return args -> {
    +        while (someCondition()) {
    +            if (!dest1In.poll(m -> {
    +                StaticMessageHeaderAccessor.getAcknowledgmentCallback(m).noAutoAck();
    +                // e.g. hand off to another thread which can perform the ack
    +                // or acknowledge(Status.REQUEUE)
    +
    +            })) {
    +                Thread.sleep(1000);
    +            }
    +        }
    +    };
    +}
    [Important]Important

    You must ack (or nack) the message at some point, to avoid resource leaks.

    [Important]Important

    Some messaging systems (such as Apache Kafka) maintain a simple offset in a log. If a delivery fails and is re-queued with StaticMessageHeaderAccessor.getAcknowledgmentCallback(m).acknowledge(Status.REQUEUE);, any later successfully ack’d messages are redelivered.

    There is also an overloaded poll method, for which the definition is as follows:

    poll(MessageHandler handler, ParameterizedTypeReference<?> type)

    The type is a conversion hint that allows the incoming message payload to be converted, as shown in the following example:

    boolean result = pollableSource.poll(received -> {
    +			Map<String, Foo> payload = (Map<String, Foo>) received.getPayload();
    +            ...
    +
    +		}, new ParameterizedTypeReference<Map<String, Foo>>() {});

    Handling Errors

    By default, an error channel is configured for the pollable source; if the callback throws an exception, an ErrorMessage is sent to the error channel (<destination>.<group>.errors); this error channel is also bridged to the global Spring Integration errorChannel.

    You can subscribe to either error channel with a @ServiceActivator to handle errors; without a subscription, the error will simply be logged and the message will be acknowledged as successful. +If the error channel service activator throws an exception, the message will be rejected (by default) and won’t be redelivered. +If the service activator throws a RequeueCurrentMessageException, the message will be requeued at the broker and will be again retrieved on a subsequent poll.

    If the listener throws a RequeueCurrentMessageException directly, the message will be requeued, as discussed above, and will not be sent to the error channels.

    29.4 Error Handling

    Errors happen, and Spring Cloud Stream provides several flexible mechanisms to handle them. +The error handling comes in two flavors:

    • application: The error handling is done within the application (custom error handler).
    • system: The error handling is delegated to the binder (re-queue, DL, and others). Note that the techniques are dependent on binder implementation and the +capability of the underlying messaging middleware.

    Spring Cloud Stream uses the Spring Retry library to facilitate successful message processing. See Section 29.4.3, “Retry Template” for more details. +However, when all fails, the exceptions thrown by the message handlers are propagated back to the binder. At that point, binder invokes custom error handler or communicates +the error back to the messaging system (re-queue, DLQ, and others).

    29.4.1 Application Error Handling

    There are two types of application-level error handling. Errors can be handled at each binding subscription or a global handler can handle all the binding subscription errors. Let’s review the details.

    Figure 29.1. A Spring Cloud Stream Sink Application with Custom and Global Error Handlers

    custom vs global error channels

    For each input binding, Spring Cloud Stream creates a dedicated error channel with the following semantics <destinationName>.errors.

    [Note]Note

    The <destinationName> consists of the name of the binding (such as input) and the name of the group (such as myGroup).

    Consider the following:

    spring.cloud.stream.bindings.input.group=myGroup
    @StreamListener(Sink.INPUT) // destination name 'input.myGroup'
    +public void handle(Person value) {
    +	throw new RuntimeException("BOOM!");
    +}
    +
    +@ServiceActivator(inputChannel = Processor.INPUT + ".myGroup.errors") //channel name 'input.myGroup.errors'
    +public void error(Message<?> message) {
    +	System.out.println("Handling ERROR: " + message);
    +}

    In the preceding example the destination name is input.myGroup and the dedicated error channel name is input.myGroup.errors.

    [Note]Note

    The use of @StreamListener annotation is intended specifically to define bindings that bridge internal channels and external destinations. Given that the destination +specific error channel does NOT have an associated external destination, such channel is a prerogative of Spring Integration (SI). This means that the handler +for such destination must be defined using one of the SI handler annotations (i.e., @ServiceActivator, @Transformer etc.).

    [Note]Note

    If group is not specified anonymous group is used (something like input.anonymous.2K37rb06Q6m2r51-SPIDDQ), which is not suitable for error +handling scenarious, since you don’t know what it’s going to be until the destination is created.

    Also, in the event you are binding to the existing destination such as:

    spring.cloud.stream.bindings.input.destination=myFooDestination
    +spring.cloud.stream.bindings.input.group=myGroup

    the full destination name is myFooDestination.myGroup and then the dedicated error channel name is myFooDestination.myGroup.errors.

    Back to the example…​

    The handle(..) method, which subscribes to the channel named input, throws an exception. Given there is also a subscriber to the error channel input.myGroup.errors +all error messages are handled by this subscriber.

    If you have multiple bindings, you may want to have a single error handler. Spring Cloud Stream automatically provides support for +a global error channel by bridging each individual error channel to the channel named errorChannel, allowing a single subscriber to handle all errors, +as shown in the following example:

    @StreamListener("errorChannel")
    +public void error(Message<?> message) {
    +	System.out.println("Handling ERROR: " + message);
    +}

    This may be a convenient option if error handling logic is the same regardless of which handler produced the error.

    29.4.2 System Error Handling

    System-level error handling implies that the errors are communicated back to the messaging system and, given that not every messaging system +is the same, the capabilities may differ from binder to binder.

    That said, in this section we explain the general idea behind system level error handling and use Rabbit binder as an example. NOTE: Kafka binder provides similar +support, although some configuration properties do differ. Also, for more details and configuration options, see the individual binder’s documentation.

    If no internal error handlers are configured, the errors propagate to the binders, and the binders subsequently propagate those errors back to the messaging system. +Depending on the capabilities of the messaging system such a system may drop the message, re-queue the message for re-processing or send the failed message to DLQ. +Both Rabbit and Kafka support these concepts. However, other binders may not, so refer to your individual binder’s documentation for details on supported system-level +error-handling options.

    Drop Failed Messages

    By default, if no additional system-level configuration is provided, the messaging system drops the failed message. +While acceptable in some cases, for most cases, it is not, and we need some recovery mechanism to avoid message loss.

    DLQ - Dead Letter Queue

    DLQ allows failed messages to be sent to a special destination: - Dead Letter Queue.

    When configured, failed messages are sent to this destination for subsequent re-processing or auditing and reconciliation.

    For example, continuing on the previous example and to set up the DLQ with Rabbit binder, you need to set the following property:

    spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true

    Keep in mind that, in the above property, input corresponds to the name of the input destination binding. +The consumer indicates that it is a consumer property and auto-bind-dlq instructs the binder to configure DLQ for input +destination, which results in an additional Rabbit queue named input.myGroup.dlq.

    Once configured, all failed messages are routed to this queue with an error message similar to the following:

    delivery_mode:	1
    +headers:
    +x-death:
    +count:	1
    +reason:	rejected
    +queue:	input.hello
    +time:	1522328151
    +exchange:
    +routing-keys:	input.myGroup
    +Payload {"name”:"Bob"}

    As you can see from the above, your original message is preserved for further actions.

    However, one thing you may have noticed is that there is limited information on the original issue with the message processing. For example, you do not see a stack +trace corresponding to the original error. +To get more relevant information about the original error, you must set an additional property:

    spring.cloud.stream.rabbit.bindings.input.consumer.republish-to-dlq=true

    Doing so forces the internal error handler to intercept the error message and add additional information to it before publishing it to DLQ. +Once configured, you can see that the error message contains more information relevant to the original error, as follows:

    delivery_mode:	2
    +headers:
    +x-original-exchange:
    +x-exception-message:	has an error
    +x-original-routingKey:	input.myGroup
    +x-exception-stacktrace:	org.springframework.messaging.MessageHandlingException: nested exception is
    +      org.springframework.messaging.MessagingException: has an error, failedMessage=GenericMessage [payload=byte[15],
    +      headers={amqp_receivedDeliveryMode=NON_PERSISTENT, amqp_receivedRoutingKey=input.hello, amqp_deliveryTag=1,
    +      deliveryAttempt=3, amqp_consumerQueue=input.hello, amqp_redelivered=false, id=a15231e6-3f80-677b-5ad7-d4b1e61e486e,
    +      amqp_consumerTag=amq.ctag-skBFapilvtZhDsn0k3ZmQg, contentType=application/json, timestamp=1522327846136}]
    +      at org.spring...integ...han...MethodInvokingMessageProcessor.processMessage(MethodInvokingMessageProcessor.java:107)
    +      at. . . . .
    +Payload {"name”:"Bob"}

    This effectively combines application-level and system-level error handling to further assist with downstream troubleshooting mechanics.

    Re-queue Failed Messages

    As mentioned earlier, the currently supported binders (Rabbit and Kafka) rely on RetryTemplate to facilitate successful message processing. See Section 29.4.3, “Retry Template” for details. +However, for cases when max-attempts property is set to 1, internal reprocessing of the message is disabled. At this point, you can facilitate message re-processing (re-tries) +by instructing the messaging system to re-queue the failed message. Once re-queued, the failed message is sent back to the original handler, essentially creating a retry loop.

    This option may be feasible for cases where the nature of the error is related to some sporadic yet short-term unavailability of some resource.

    To accomplish that, you must set the following properties:

    spring.cloud.stream.bindings.input.consumer.max-attempts=1
    +spring.cloud.stream.rabbit.bindings.input.consumer.requeue-rejected=true

    In the preceding example, the max-attempts set to 1 essentially disabling internal re-tries and requeue-rejected (short for requeue rejected messages) is set to true. +Once set, the failed message is resubmitted to the same handler and loops continuously or until the handler throws AmqpRejectAndDontRequeueException +essentially allowing you to build your own re-try logic within the handler itself.

    29.4.3 Retry Template

    The RetryTemplate is part of the Spring Retry library. +While it is out of scope of this document to cover all of the capabilities of the RetryTemplate, we will mention the following consumer properties that are specifically related to +the RetryTemplate:

    maxAttempts

    The number of attempts to process the message.

    Default: 3.

    backOffInitialInterval

    The backoff initial interval on retry.

    Default 1000 milliseconds.

    backOffMaxInterval

    The maximum backoff interval.

    Default 10000 milliseconds.

    backOffMultiplier

    The backoff multiplier.

    Default 2.0.

    defaultRetryable

    Whether exceptions thrown by the listener that are not listed in the retryableExceptions are retryable.

    Default: true.

    retryableExceptions

    A map of Throwable class names in the key and a boolean in the value. +Specify those exceptions (and subclasses) that will or won’t be retried. +Also see defaultRetriable. +Example: spring.cloud.stream.bindings.input.consumer.retryable-exceptions.java.lang.IllegalStateException=false.

    Default: empty.

    While the preceding settings are sufficient for majority of the customization requirements, they may not satisfy certain complex requirements at, which +point you may want to provide your own instance of the RetryTemplate. To do so configure it as a bean in your application configuration. The application provided +instance will override the one provided by the framework. Also, to avoid conflicts you must qualify the instance of the RetryTemplate you want to be used by the binder +as @StreamRetryTemplate. For example,

    @StreamRetryTemplate
    +public RetryTemplate myRetryTemplate() {
    +    return new RetryTemplate();
    +}

    As you can see from the above example you don’t need to annotate it with @Bean since @StreamRetryTemplate is a qualified @Bean.

    29.5 Reactive Programming Support

    Spring Cloud Stream also supports the use of reactive APIs where incoming and outgoing data is handled as continuous data flows. +Support for reactive APIs is available through spring-cloud-stream-reactive, which needs to be added explicitly to your project.

    The programming model with reactive APIs is declarative. Instead of specifying how each individual message should be handled, you can use operators that describe functional transformations from inbound to outbound data flows.

    At present Spring Cloud Stream supports the only the Reactor API. +In the future, we intend to support a more generic model based on Reactive Streams.

    The reactive programming model also uses the @StreamListener annotation for setting up reactive handlers. +The differences are that:

    • The @StreamListener annotation must not specify an input or output, as they are provided as arguments and return values from the method.
    • The arguments of the method must be annotated with @Input and @Output, indicating which input or output the incoming and outgoing data flows connect to, respectively.
    • The return value of the method, if any, is annotated with @Output, indicating the input where data should be sent.
    [Note]Note

    Reactive programming support requires Java 1.8.

    [Note]Note

    As of Spring Cloud Stream 1.1.1 and later (starting with release train Brooklyn.SR2), reactive programming support requires the use of Reactor 3.0.4.RELEASE and higher. +Earlier Reactor versions (including 3.0.1.RELEASE, 3.0.2.RELEASE and 3.0.3.RELEASE) are not supported. +spring-cloud-stream-reactive transitively retrieves the proper version, but it is possible for the project structure to manage the version of the io.projectreactor:reactor-core to an earlier release, especially when using Maven. +This is the case for projects generated by using Spring Initializr with Spring Boot 1.x, which overrides the Reactor version to 2.0.8.RELEASE. +In such cases, you must ensure that the proper version of the artifact is released. +You can do so by adding a direct dependency on io.projectreactor:reactor-core with a version of 3.0.4.RELEASE or later to your project.

    [Note]Note

    The use of term, reactive, currently refers to the reactive APIs being used and not to the execution model being reactive (that is, the bound endpoints still use a 'push' rather than a 'pull' model). While some backpressure support is provided by the use of Reactor, we do intend, in a future release, to support entirely reactive pipelines by the use of native reactive clients for the connected middleware.

    29.5.1 Reactor-based Handlers

    A Reactor-based handler can have the following argument types:

    • For arguments annotated with @Input, it supports the Reactor Flux type. +The parameterization of the inbound Flux follows the same rules as in the case of individual message handling: It can be the entire Message, a POJO that can be the Message payload, or a POJO that is the result of a transformation based on the Message content-type header. Multiple inputs are provided.
    • For arguments annotated with Output, it supports the FluxSender type, which connects a Flux produced by the method with an output. Generally speaking, specifying outputs as arguments is only recommended when the method can have multiple outputs.

    A Reactor-based handler supports a return type of Flux. In that case, it must be annotated with @Output. We recommend using the return value of the method when a single output Flux is available.

    The following example shows a Reactor-based Processor:

    @EnableBinding(Processor.class)
    +@EnableAutoConfiguration
    +public static class UppercaseTransformer {
    +
    +  @StreamListener
    +  @Output(Processor.OUTPUT)
    +  public Flux<String> receive(@Input(Processor.INPUT) Flux<String> input) {
    +    return input.map(s -> s.toUpperCase());
    +  }
    +}

    The same processor using output arguments looks like the following example:

    @EnableBinding(Processor.class)
    +@EnableAutoConfiguration
    +public static class UppercaseTransformer {
    +
    +  @StreamListener
    +  public void receive(@Input(Processor.INPUT) Flux<String> input,
    +     @Output(Processor.OUTPUT) FluxSender output) {
    +     output.send(input.map(s -> s.toUpperCase()));
    +  }
    +}

    29.5.2 Reactive Sources

    Spring Cloud Stream reactive support also provides the ability for creating reactive sources through the @StreamEmitter annotation. +By using the @StreamEmitter annotation, a regular source may be converted to a reactive one. +@StreamEmitter is a method level annotation that marks a method to be an emitter to outputs declared with @EnableBinding. +You cannot use the @Input annotation along with @StreamEmitter, as the methods marked with this annotation are not listening for any input. Rather, methods marked with @StreamEmitter generate output. +Following the same programming model used in @StreamListener, @StreamEmitter also allows flexible ways of using the @Output annotation, depending on whether the method has any arguments, a return type, and other considerations.

    The remainder of this section contains examples of using the @StreamEmitter annotation in various styles.

    The following example emits the Hello, World message every millisecond and publishes to a Reactor Flux:

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  @Output(Source.OUTPUT)
    +  public Flux<String> emit() {
    +    return Flux.intervalMillis(1)
    +            .map(l -> "Hello World");
    +  }
    +}

    In the preceding example, the resulting messages in the Flux are sent to the output channel of the Source.

    The next example is another flavor of an @StreamEmmitter that sends a Reactor Flux. +Instead of returning a Flux, the following method uses a FluxSender to programmatically send a Flux from a source:

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  @Output(Source.OUTPUT)
    +  public void emit(FluxSender output) {
    +    output.send(Flux.intervalMillis(1)
    +            .map(l -> "Hello World"));
    +  }
    +}

    The next example is exactly same as the above snippet in functionality and style. +However, instead of using an explicit @Output annotation on the method, it uses the annotation on the method parameter.

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  public void emit(@Output(Source.OUTPUT) FluxSender output) {
    +    output.send(Flux.intervalMillis(1)
    +            .map(l -> "Hello World"));
    +  }
    +}

    The last example in this section is yet another flavor of writing reacting sources by using the Reactive Streams Publisher API and taking advantage of the support for it in Spring Integration Java DSL. +The Publisher in the following example still uses Reactor Flux under the hood, but, from an application perspective, that is transparent to the user and only needs Reactive Streams and Java DSL for Spring Integration:

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  @Output(Source.OUTPUT)
    +  @Bean
    +  public Publisher<Message<String>> emit() {
    +    return IntegrationFlows.from(() ->
    +                new GenericMessage<>("Hello World"),
    +        e -> e.poller(p -> p.fixedDelay(1)))
    +        .toReactivePublisher();
    +  }
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__propagation.html b/Greenwich.SR5/multi/multi__propagation.html new file mode 100644 index 00000000..d89ff646 --- /dev/null +++ b/Greenwich.SR5/multi/multi__propagation.html @@ -0,0 +1,96 @@ + + + 54. Propagation

    54. Propagation

    Propagation is needed to ensure activities originating from the same root are collected together in the same trace. +The most common propagation approach is to copy a trace context from a client by sending an RPC request to a server receiving it.

    For example, when a downstream HTTP call is made, its trace context is encoded as request headers and sent along with it, as shown in the following image:

       Client Span                                                Server Span
    +┌──────────────────┐                                       ┌──────────────────┐
    +│                  │                                       │                  │
    +│   TraceContext   │           Http Request Headers        │   TraceContext   │
    +│ ┌──────────────┐ │          ┌───────────────────┐        │ ┌──────────────┐ │
    +│ │ TraceId      │ │          │ X─B3─TraceId      │        │ │ TraceId      │ │
    +│ │              │ │          │                   │        │ │              │ │
    +│ │ ParentSpanId │ │ Extract  │ X─B3─ParentSpanId │ Inject │ │ ParentSpanId │ │
    +│ │              ├─┼─────────>│                   ├────────┼>│              │ │
    +│ │ SpanId       │ │          │ X─B3─SpanId       │        │ │ SpanId       │ │
    +│ │              │ │          │                   │        │ │              │ │
    +│ │ Sampled      │ │          │ X─B3─Sampled      │        │ │ Sampled      │ │
    +│ └──────────────┘ │          └───────────────────┘        │ └──────────────┘ │
    +│                  │                                       │                  │
    +└──────────────────┘                                       └──────────────────┘

    The names above are from B3 Propagation, which is built-in to Brave and has implementations in many languages and frameworks.

    Most users use a framework interceptor to automate propagation. +The next two examples show how that might work for a client and a server.

    The following example shows how client-side propagation might work:

    @Autowired Tracing tracing;
    +
    +// configure a function that injects a trace context into a request
    +injector = tracing.propagation().injector(Request.Builder::addHeader);
    +
    +// before a request is sent, add the current span's context to it
    +injector.inject(span.context(), request);

    The following example shows how server-side propagation might work:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// configure a function that extracts the trace context from a request
    +extractor = tracing.propagation().extractor(Request::getHeader);
    +
    +// when a server receives a request, it joins or starts a new trace
    +span = tracer.nextSpan(extractor.extract(request));

    54.1 Propagating extra fields

    Sometimes you need to propagate extra fields, such as a request ID or an alternate trace context. +For example, if you are in a Cloud Foundry environment, you might want to pass the request ID, as shown in the following example:

    // when you initialize the builder, define the extra field you want to propagate
    +Tracing.newBuilder().propagationFactory(
    +  ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-vcap-request-id")
    +);
    +
    +// later, you can tag that request ID or use it in log correlation
    +requestId = ExtraFieldPropagation.get("x-vcap-request-id");

    You may also need to propagate a trace context that you are not using. +For example, you may be in an Amazon Web Services environment but not be reporting data to X-Ray. +To ensure X-Ray can co-exist correctly, pass-through its tracing header, as shown in the following example:

    tracingBuilder.propagationFactory(
    +  ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-amzn-trace-id")
    +);
    [Tip]Tip

    In Spring Cloud Sleuth all elements of the tracing builder Tracing.newBuilder() +are defined as beans. So if you want to pass a custom PropagationFactory, it’s enough +for you to create a bean of that type and we will set it in the Tracing bean.

    54.1.1 Prefixed fields

    If they follow a common pattern, you can also prefix fields. +The following example shows how to propagate x-vcap-request-id the field as-is but send the country-code and user-id fields on the wire as x-baggage-country-code and x-baggage-user-id, respectively:

    Tracing.newBuilder().propagationFactory(
    +  ExtraFieldPropagation.newFactoryBuilder(B3Propagation.FACTORY)
    +                       .addField("x-vcap-request-id")
    +                       .addPrefixedFields("x-baggage-", Arrays.asList("country-code", "user-id"))
    +                       .build()
    +);

    Later, you can call the following code to affect the country code of the current trace context:

    ExtraFieldPropagation.set("x-country-code", "FO");
    +String countryCode = ExtraFieldPropagation.get("x-country-code");

    Alternatively, if you have a reference to a trace context, you can use it explicitly, as shown in the following example:

    ExtraFieldPropagation.set(span.context(), "x-country-code", "FO");
    +String countryCode = ExtraFieldPropagation.get(span.context(), "x-country-code");
    [Important]Important

    A difference from previous versions of Sleuth is that, with Brave, you must pass the list of baggage keys. +There are two properties to achieve this. +With the spring.sleuth.baggage-keys, you set keys that get prefixed with baggage- for HTTP calls and baggage_ for messaging. +You can also use the spring.sleuth.propagation-keys property to pass a list of prefixed keys that are whitelisted without any prefix. +Notice that there’s no x- in front of the header keys.

    In order to automatically set the baggage values to Slf4j’s MDC, you have to set +the spring.sleuth.log.slf4j.whitelisted-mdc-keys property with a list of whitelisted +baggage and propagation keys. E.g. spring.sleuth.log.slf4j.whitelisted-mdc-keys=foo will set the value of the foo baggage into MDC.

    [Important]Important

    Remember that adding entries to MDC can drastically decrease the performance of your application!

    If you want to add the baggage entries as tags, to make it possible to search for spans via the baggage entries, you can set the value of +spring.sleuth.propagation.tag.whitelisted-keys with a list of whitelisted baggage keys. To disable the feature you have to pass the spring.sleuth.propagation.tag.enabled=false property.

    54.1.2 Extracting a Propagated Context

    The TraceContext.Extractor<C> reads trace identifiers and sampling status from an incoming request or message. +The carrier is usually a request object or headers.

    This utility is used in standard instrumentation (such as HttpServerHandler) but can also be used for custom RPC or messaging code.

    TraceContextOrSamplingFlags is usually used only with Tracer.nextSpan(extracted), unless you are +sharing span IDs between a client and a server.

    54.1.3 Sharing span IDs between Client and Server

    A normal instrumentation pattern is to create a span representing the server side of an RPC. +Extractor.extract might return a complete trace context when applied to an incoming client request. +Tracer.joinSpan attempts to continue this trace, using the same span ID if supported or creating a child span +if not. When the span ID is shared, the reported data includes a flag saying so.

    The following image shows an example of B3 propagation:

                                  ┌───────────────────┐      ┌───────────────────┐
    + Incoming Headers             │   TraceContext    │      │   TraceContext    │
    +┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │
    +│ X─B3-TraceId      │─────────┼─┼> TraceId      │ │──────┼─┼> TraceId      │ │
    +│                   │         │ │               │ │      │ │               │ │
    +│ X─B3-ParentSpanId │─────────┼─┼> ParentSpanId │ │──────┼─┼> ParentSpanId │ │
    +│                   │         │ │               │ │      │ │               │ │
    +│ X─B3-SpanId       │─────────┼─┼> SpanId       │ │──────┼─┼> SpanId       │ │
    +└───────────────────┘         │ │               │ │      │ │               │ │
    +                              │ │               │ │      │ │  Shared: true │ │
    +                              │ └───────────────┘ │      │ └───────────────┘ │
    +                              └───────────────────┘      └───────────────────┘

    Some propagation systems forward only the parent span ID, detected when Propagation.Factory.supportsJoin() == false. +In this case, a new span ID is always provisioned, and the incoming context determines the parent ID.

    The following image shows an example of AWS propagation:

                                  ┌───────────────────┐      ┌───────────────────┐
    + x-amzn-trace-id              │   TraceContext    │      │   TraceContext    │
    +┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │
    +│ Root              │─────────┼─┼> TraceId      │ │──────┼─┼> TraceId      │ │
    +│                   │         │ │               │ │      │ │               │ │
    +│ Parent            │─────────┼─┼> SpanId       │ │──────┼─┼> ParentSpanId │ │
    +└───────────────────┘         │ └───────────────┘ │      │ │               │ │
    +                              └───────────────────┘      │ │  SpanId: New  │ │
    +                                                         │ └───────────────┘ │
    +                                                         └───────────────────┘

    Note: Some span reporters do not support sharing span IDs. +For example, if you set Tracing.Builder.spanReporter(amazonXrayOrGoogleStackdrive), you should disable join by setting Tracing.Builder.supportsJoin(false). +Doing so forces a new child span on Tracer.joinSpan().

    54.1.4 Implementing Propagation

    TraceContext.Extractor<C> is implemented by a Propagation.Factory plugin. +Internally, this code creates the union type, TraceContextOrSamplingFlags, with one of the following: +* TraceContext if trace and span IDs were present. +* TraceIdContext if a trace ID was present but span IDs were not present. +* SamplingFlags if no identifiers were present.

    Some Propagation implementations carry extra data from the point of extraction (for example, reading incoming headers) to injection (for example, writing outgoing headers). +For example, it might carry a request ID. +When implementations have extra data, they handle it as follows: +* If a TraceContext were extracted, add the extra data as TraceContext.extra(). +* Otherwise, add it as TraceContextOrSamplingFlags.extra(), which Tracer.nextSpan handles.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__push_notifications_and_spring_cloud_bus.html b/Greenwich.SR5/multi/multi__push_notifications_and_spring_cloud_bus.html new file mode 100644 index 00000000..8578a1b8 --- /dev/null +++ b/Greenwich.SR5/multi/multi__push_notifications_and_spring_cloud_bus.html @@ -0,0 +1,11 @@ + + + 9. Push Notifications and Spring Cloud Bus

    9. Push Notifications and Spring Cloud Bus

    Many source code repository providers (such as Github, Gitlab, Gitea, Gitee, Gogs, or Bitbucket) notify you of changes in a repository through a webhook. +You can configure the webhook through the provider’s user interface as a URL and a set of events in which you are interested. +For instance, Github uses a POST to the webhook with a JSON body containing a list of commits and a header (X-Github-Event) set to push. +If you add a dependency on the spring-cloud-config-monitor library and activate the Spring Cloud Bus in your Config Server, then a /monitor endpoint is enabled.

    When the webhook is activated, the Config Server sends a RefreshRemoteApplicationEvent targeted at the applications it thinks might have changed. +The change detection can be strategized. +However, by default, it looks for changes in files that match the application name (for example, foo.properties is targeted at the foo application, while application.properties is targeted at all applications). +The strategy to use when you want to override the behavior is PropertyPathNotificationExtractor, which accepts the request headers and body as parameters and returns a list of file paths that changed.

    The default configuration works out of the box with Github, Gitlab, Gitea, Gitee, Gogs or Bitbucket. +In addition to the JSON notifications from Github, Gitlab, Gitee, or Bitbucket, you can trigger a change notification by POSTing to /monitor with form-encoded body parameters in the pattern of path={application}. +Doing so broadcasts to applications matching the {application} pattern (which can contain wildcards).

    [Note]Note

    The RefreshRemoteApplicationEvent is transmitted only if the spring-cloud-bus is activated in both the Config Server and in the client application.

    [Note]Note

    The default configuration also detects filesystem changes in local git repositories. In that case, the webhook is not used. However, as soon as you edit a config file, a refresh is broadcast.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__quick_start.html b/Greenwich.SR5/multi/multi__quick_start.html new file mode 100644 index 00000000..cdfa506f --- /dev/null +++ b/Greenwich.SR5/multi/multi__quick_start.html @@ -0,0 +1,83 @@ + + + 4. Quick Start

    4. Quick Start

    This quick start walks through using both the server and the client of Spring Cloud Config Server.

    First, start the server, as follows:

    $ cd spring-cloud-config-server
    +$ ../mvnw spring-boot:run

    The server is a Spring Boot application, so you can run it from your IDE if you prefer to do so (the main class is ConfigServerApplication).

    Next try out a client, as follows:

    $ curl localhost:8888/foo/development
    +{"name":"foo","label":"master","propertySources":[
    +  {"name":"https://github.com/scratches/config-repo/foo-development.properties","source":{"bar":"spam"}},
    +  {"name":"https://github.com/scratches/config-repo/foo.properties","source":{"foo":"bar"}}
    +]}

    The default strategy for locating property sources is to clone a git repository (at spring.cloud.config.server.git.uri) and use it to initialize a mini SpringApplication. +The mini-application’s Environment is used to enumerate property sources and publish them at a JSON endpoint.

    The HTTP service has resources in the following form:

    /{application}/{profile}[/{label}]
    +/{application}-{profile}.yml
    +/{label}/{application}-{profile}.yml
    +/{application}-{profile}.properties
    +/{label}/{application}-{profile}.properties

    where application is injected as the spring.config.name in the SpringApplication (what is normally application in a regular Spring Boot app), profile is an active profile (or comma-separated list of properties), and label is an optional git label (defaults to master.)

    Spring Cloud Config Server pulls configuration for remote clients from various sources. The following example gets configuration from a git repository (which must be provided), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo

    Other sources are any JDBC compatible database, Subversion, Hashicorp Vault, Credhub and local filesystems.

    4.1 Client Side Usage

    To use these features in an application, you can build it as a Spring Boot application that depends on spring-cloud-config-client (for an example, see the test cases for the config-client or the sample application). +The most convenient way to add the dependency is with a Spring Boot starter org.springframework.cloud:spring-cloud-starter-config. +There is also a parent pom and BOM (spring-cloud-starter-parent) for Maven users and a Spring IO version management properties file for Gradle and Spring CLI users. The following example shows a typical Maven configuration:

    pom.xml.  +

       <parent>
    +       <groupId>org.springframework.boot</groupId>
    +       <artifactId>spring-boot-starter-parent</artifactId>
    +       <version>{spring-boot-docs-version}</version>
    +       <relativePath /> <!-- lookup parent from repository -->
    +   </parent>
    +
    +<dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>{spring-cloud-version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>
    +
    +<dependencies>
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-starter-config</artifactId>
    +	</dependency>
    +	<dependency>
    +		<groupId>org.springframework.boot</groupId>
    +		<artifactId>spring-boot-starter-test</artifactId>
    +		<scope>test</scope>
    +	</dependency>
    +</dependencies>
    +
    +<build>
    +	<plugins>
    +           <plugin>
    +               <groupId>org.springframework.boot</groupId>
    +               <artifactId>spring-boot-maven-plugin</artifactId>
    +           </plugin>
    +	</plugins>
    +</build>
    +
    +   <!-- repositories also needed for snapshots and milestones -->

    +

    Now you can create a standard Spring Boot application, such as the following HTTP server:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello World!";
    +    }
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(Application.class, args);
    +    }
    +
    +}

    When this HTTP server runs, it picks up the external configuration from the default local config server (if it is running) on port 8888. +To modify the startup behavior, you can change the location of the config server by using bootstrap.properties (similar to application.properties but for the bootstrap phase of an application context), as shown in the following example:

    spring.cloud.config.uri: http://myconfigserver.com

    By default, if no application name is set, application will be used. To modify the name, the following property can be added to the bootstrap.properties file:

    spring.application.name: myapp
    [Note]Note

    When setting the property ${spring.application.name} do not prefix your app name with the reserved word application- to prevent issues resolving the correct property source.

    The bootstrap properties show up in the /env endpoint as a high-priority property source, as shown in the following example.

    $ curl localhost:8080/env
    +{
    +  "profiles":[],
    +  "configService:https://github.com/spring-cloud-samples/config-repo/bar.properties":{"foo":"bar"},
    +  "servletContextInitParams":{},
    +  "systemProperties":{...},
    +  ...
    +}

    A property source called ``configService:<URL of remote repository>/<file name> contains the foo property with a value of bar and is highest priority.

    [Note]Note

    The URL in the property source name is the git repository, not the config server URL.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__quick_start_2.html b/Greenwich.SR5/multi/multi__quick_start_2.html new file mode 100644 index 00000000..c809837d --- /dev/null +++ b/Greenwich.SR5/multi/multi__quick_start_2.html @@ -0,0 +1,53 @@ + + + 25. Quick Start

    25. Quick Start

    You can try Spring Cloud Stream in less then 5 min even before you jump into any details by following this three-step guide.

    We show you how to create a Spring Cloud Stream application that receives messages coming from the messaging middleware of your choice (more on this later) and logs received messages to the console. +We call it LoggingConsumer. +While not very practical, it provides a good introduction to some of the main concepts +and abstractions, making it easier to digest the rest of this user guide.

    The three steps are as follows:

    25.1 Creating a Sample Application by Using Spring Initializr

    To get started, visit the Spring Initializr. From there, you can generate our LoggingConsumer application. To do so:

    1. In the Dependencies section, start typing stream. +When the Cloud Stream option should appears, select it.
    2. Start typing either 'kafka' or 'rabbit'.
    3. Select Kafka or RabbitMQ.

      Basically, you choose the messaging middleware to which your application binds. +We recommend using the one you have already installed or feel more comfortable with installing and running. +Also, as you can see from the Initilaizer screen, there are a few other options you can choose. +For example, you can choose Gradle as your build tool instead of Maven (the default).

    4. In the Artifact field, type 'logging-consumer'.

      The value of the Artifact field becomes the application name. +If you chose RabbitMQ for the middleware, your Spring Initializr should now be as follows:

      stream initializr
    5. Click the Generate Project button.

      Doing so downloads the zipped version of the generated project to your hard drive.

    6. Unzip the file into the folder you want to use as your project directory.
    [Tip]Tip

    We encourage you to explore the many possibilities available in the Spring Initializr. +It lets you create many different kinds of Spring applications.

    25.2 Importing the Project into Your IDE

    Now you can import the project into your IDE. +Keep in mind that, depending on the IDE, you may need to follow a specific import procedure. +For example, depending on how the project was generated (Maven or Gradle), you may need to follow specific import procedure (for example, in Eclipse or STS, you need to use File → Import → Maven → Existing Maven Project).

    Once imported, the project must have no errors of any kind. Also, src/main/java should contain com.example.loggingconsumer.LoggingConsumerApplication.

    Technically, at this point, you can run the application’s main class. +It is already a valid Spring Boot application. +However, it does not do anything, so we want to add some code.

    25.3 Adding a Message Handler, Building, and Running

    Modify the com.example.loggingconsumer.LoggingConsumerApplication class to look as follows:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class LoggingConsumerApplication {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(LoggingConsumerApplication.class, args);
    +	}
    +
    +	@StreamListener(Sink.INPUT)
    +	public void handle(Person person) {
    +		System.out.println("Received: " + person);
    +	}
    +
    +	public static class Person {
    +		private String name;
    +		public String getName() {
    +			return name;
    +		}
    +		public void setName(String name) {
    +			this.name = name;
    +		}
    +		public String toString() {
    +			return this.name;
    +		}
    +	}
    +}

    As you can see from the preceding listing:

    • We have enabled Sink binding (input-no-output) by using @EnableBinding(Sink.class). +Doing so signals to the framework to initiate binding to the messaging middleware, where it automatically creates the destination (that is, queue, topic, and others) that are bound to the Sink.INPUT channel.
    • We have added a handler method to receive incoming messages of type Person. +Doing so lets you see one of the core features of the framework: It tries to automatically convert incoming message payloads to type Person.

    You now have a fully functional Spring Cloud Stream application that does listens for messages. +From here, for simplicity, we assume you selected RabbitMQ in step one. +Assuming you have RabbitMQ installed and running, you can start the application by running its main method in your IDE.

    You should see following output:

    	--- [ main] c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg, bound to: input
    +	--- [ main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
    +	--- [ main] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#2a3a299:0/SimpleConnection@66c83fc8. . .
    +	. . .
    +	--- [ main] o.s.i.a.i.AmqpInboundChannelAdapter      : started inbound.input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg
    +	. . .
    +	--- [ main] c.e.l.LoggingConsumerApplication         : Started LoggingConsumerApplication in 2.531 seconds (JVM running for 2.897)

    Go to the RabbitMQ management console or any other RabbitMQ client and send a message to input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg. +The anonymous.CbMIwdkJSBO1ZoPDOtHtCg part represents the group name and is generated, so it is bound to be different in your environment. +For something more predictable, you can use an explicit group name by setting spring.cloud.stream.bindings.input.group=hello (or whatever name you like).

    The contents of the message should be a JSON representation of the Person class, as follows:

    {"name":"Sam Spade"}

    Then, in your console, you should see:

    Received: Sam Spade

    You can also build and package your application into a boot jar (by using ./mvnw clean install) and run the built JAR by using the java -jar command.

    Now you have a working (albeit very basic) Spring Cloud Stream application.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__quick_start_3.html b/Greenwich.SR5/multi/multi__quick_start_3.html new file mode 100644 index 00000000..ddf4c3ee --- /dev/null +++ b/Greenwich.SR5/multi/multi__quick_start_3.html @@ -0,0 +1,23 @@ + + + 42. Quick Start

    42. Quick Start

    Spring Cloud Bus works by adding Spring Boot autconfiguration if it detects itself on the +classpath. To enable the bus, add spring-cloud-starter-bus-amqp or +spring-cloud-starter-bus-kafka to your dependency management. Spring Cloud takes care of +the rest. Make sure the broker (RabbitMQ or Kafka) is available and configured. When +running on localhost, you need not do anything. If you run remotely, use Spring Cloud +Connectors or Spring Boot conventions to define the broker credentials, as shown in the +following example for Rabbit:

    application.yml.  +

    spring:
    +  rabbitmq:
    +    host: mybroker.com
    +    port: 5672
    +    username: user
    +    password: secret

    +

    The bus currently supports sending messages to all nodes listening or all nodes for a +particular service (as defined by Eureka). The /bus/* actuator namespace has some HTTP +endpoints. Currently, two are implemented. The first, /bus/env, sends key/value pairs to +update each node’s Spring Environment. The second, /bus/refresh, reloads each +application’s configuration, as though they had all been pinged on their /refresh +endpoint.

    [Note]Note

    The Spring Cloud Bus starters cover Rabbit and Kafka, because those are the two most +common implementations. However, Spring Cloud Stream is quite flexible, and the binder +works with spring-cloud-bus.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__quick_start_4.html b/Greenwich.SR5/multi/multi__quick_start_4.html new file mode 100644 index 00000000..71314fae --- /dev/null +++ b/Greenwich.SR5/multi/multi__quick_start_4.html @@ -0,0 +1,37 @@ + + + 100. Quick Start

    100. Quick Start

    Prerequisites

    To get started with Vault and this guide you need a +*NIX-like operating systems that provides:

    • wget, openssl and unzip
    • at least Java 7 and a properly configured JAVA_HOME environment variable

    Install Vault

    $ src/test/bash/install_vault.sh

    Create SSL certificates for Vault

    $ src/test/bash/create_certificates.sh
    [Note]Note

    create_certificates.sh creates certificates in work/ca and a JKS truststore work/keystore.jks. If you want to run Spring Cloud Vault using this quickstart guide you need to configure the truststore the spring.cloud.vault.ssl.trust-store property to file:work/keystore.jks.

    Start Vault server

    $ src/test/bash/local_run_vault.sh

    Vault is started listening on 0.0.0.0:8200 using the inmem storage and +https. +Vault is sealed and not initialized when starting up.

    [Note]Note

    If you want to run tests, leave Vault uninitialized. The tests will +initialize Vault and create a root token 00000000-0000-0000-0000-000000000000.

    If you want to use Vault for your application or give it a try then you need to initialize it first.

    $ export VAULT_ADDR="https://localhost:8200"
    +$ export VAULT_SKIP_VERIFY=true # Don't do this for production
    +$ vault init

    You should see something like:

    Key 1: 7149c6a2e16b8833f6eb1e76df03e47f6113a3288b3093faf5033d44f0e70fe701
    +Key 2: 901c534c7988c18c20435a85213c683bdcf0efcd82e38e2893779f152978c18c02
    +Key 3: 03ff3948575b1165a20c20ee7c3e6edf04f4cdbe0e82dbff5be49c63f98bc03a03
    +Key 4: 216ae5cc3ddaf93ceb8e1d15bb9fc3176653f5b738f5f3d1ee00cd7dccbe926e04
    +Key 5: b2898fc8130929d569c1677ee69dc5f3be57d7c4b494a6062693ce0b1c4d93d805
    +Initial Root Token: 19aefa97-cccc-bbbb-aaaa-225940e63d76
    +
    +Vault initialized with 5 keys and a key threshold of 3. Please
    +securely distribute the above keys. When the Vault is re-sealed,
    +restarted, or stopped, you must provide at least 3 of these keys
    +to unseal it again.
    +
    +Vault does not store the master key. Without at least 3 keys,
    +your Vault will remain permanently sealed.

    Vault will initialize and return a set of unsealing keys and the root token. +Pick 3 keys and unseal Vault. Store the Vault token in the VAULT_TOKEN + environment variable.

    $ vault unseal (Key 1)
    +$ vault unseal (Key 2)
    +$ vault unseal (Key 3)
    +$ export VAULT_TOKEN=(Root token)
    +# Required to run Spring Cloud Vault tests after manual initialization
    +$ vault token-create -id="00000000-0000-0000-0000-000000000000" -policy="root"

    Spring Cloud Vault accesses different resources. By default, the secret +backend is enabled which accesses secret config settings via JSON endpoints.

    The HTTP service has resources in the form:

    /secret/{application}/{profile}
    +/secret/{application}
    +/secret/{defaultContext}/{profile}
    +/secret/{defaultContext}

    where the "application" is injected as the spring.application.name in the +SpringApplication (i.e. what is normally "application" in a regular +Spring Boot app), "profile" is an active profile (or comma-separated +list of properties). Properties retrieved from Vault will be used "as-is" +without further prefixing of the property names.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__quickstart.html b/Greenwich.SR5/multi/multi__quickstart.html new file mode 100644 index 00000000..e01efbd2 --- /dev/null +++ b/Greenwich.SR5/multi/multi__quickstart.html @@ -0,0 +1,72 @@ + + + 81. Quickstart

    81. Quickstart

    81.1 OAuth2 Single Sign On

    Here’s a Spring Cloud "Hello World" app with HTTP Basic +authentication and a single user account:

    app.groovy.  +

    @Grab('spring-boot-starter-security')
    +@Controller
    +class Application {
    +
    +  @RequestMapping('/')
    +  String home() {
    +    'Hello World'
    +  }
    +
    +}

    +

    You can run it with spring run app.groovy and watch the logs for the password (username is "user"). So far this is just the default for a Spring Boot app.

    Here’s a Spring Cloud app with OAuth2 SSO:

    app.groovy.  +

    @Controller
    +@EnableOAuth2Sso
    +class Application {
    +
    +  @RequestMapping('/')
    +  String home() {
    +    'Hello World'
    +  }
    +
    +}

    +

    Spot the difference? This app will actually behave exactly the same as +the previous one, because it doesn’t know it’s OAuth2 credentals +yet.

    You can register an app in github quite easily, so try that if you +want a production app on your own domain. If you are happy to test on +localhost:8080, then set up these properties in your application +configuration:

    application.yml.  +

    security:
    +  oauth2:
    +    client:
    +      clientId: bd1c0a783ccdd1c9b9e4
    +      clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    +      accessTokenUri: https://github.com/login/oauth/access_token
    +      userAuthorizationUri: https://github.com/login/oauth/authorize
    +      clientAuthenticationScheme: form
    +    resource:
    +      userInfoUri: https://api.github.com/user
    +      preferTokenInfo: false

    +

    run the app above and it will redirect to github for authorization. If +you are already signed into github you won’t even notice that it has +authenticated. These credentials will only work if your app is +running on port 8080.

    To limit the scope that the client asks for when it obtains an access token +you can set security.oauth2.client.scope (comma separated or an array in YAML). By +default the scope is empty and it is up to to Authorization Server to +decide what the defaults should be, usually depending on the settings in +the client registration that it holds.

    [Note]Note

    The examples above are all Groovy scripts. If you want to write the +same code in Java (or Groovy) you need to add Spring Security OAuth2 +to the classpath (e.g. see the +sample here).

    81.2 OAuth2 Protected Resource

    You want to protect an API resource with an OAuth2 token? Here’s a +simple example (paired with the client above):

    app.groovy.  +

    @Grab('spring-cloud-starter-security')
    +@RestController
    +@EnableResourceServer
    +class Application {
    +
    +  @RequestMapping('/')
    +  def home() {
    +    [message: 'Hello World']
    +  }
    +
    +}

    +

    and

    application.yml.  +

    security:
    +  oauth2:
    +    resource:
    +      userInfoUri: https://api.github.com/user
    +      preferTokenInfo: false

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__rabbitmq_binder.html b/Greenwich.SR5/multi/multi__rabbitmq_binder.html new file mode 100644 index 00000000..6d29a5a7 --- /dev/null +++ b/Greenwich.SR5/multi/multi__rabbitmq_binder.html @@ -0,0 +1,428 @@ + + + 41. RabbitMQ Binder

    41. RabbitMQ Binder

    41.1 Usage

    To use the RabbitMQ binder, you can add it to your Spring Cloud Stream application, by using the following Maven coordinates:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    +</dependency>

    Alternatively, you can use the Spring Cloud Stream RabbitMQ Starter, as follows:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    +</dependency>

    41.2 RabbitMQ Binder Overview

    The following simplified diagram shows how the RabbitMQ binder operates:

    Figure 41.1. RabbitMQ Binder

    rabbit binder

    By default, the RabbitMQ Binder implementation maps each destination to a TopicExchange. +For each consumer group, a Queue is bound to that TopicExchange. +Each consumer instance has a corresponding RabbitMQ Consumer instance for its group’s Queue. +For partitioned producers and consumers, the queues are suffixed with the partition index and use the partition index as the routing key. +For anonymous consumers (those with no group property), an auto-delete queue (with a randomized unique name) is used.

    By using the optional autoBindDlq option, you can configure the binder to create and configure dead-letter queues (DLQs) (and a dead-letter exchange DLX, as well as routing infrastructure). +By default, the dead letter queue has the name of the destination, appended with .dlq. +If retry is enabled (maxAttempts > 1), failed messages are delivered to the DLQ after retries are exhausted. +If retry is disabled (maxAttempts = 1), you should set requeueRejected to false (the default) so that failed messages are routed to the DLQ, instead of being re-queued. +In addition, republishToDlq causes the binder to publish a failed message to the DLQ (instead of rejecting it). +This feature lets additional information (such as the stack trace in the x-exception-stacktrace header) be added to the message in headers. +This option does not need retry enabled. +You can republish a failed message after just one attempt. +Starting with version 1.2, you can configure the delivery mode of republished messages. +See the republishDeliveryMode property.

    [Important]Important

    Setting requeueRejected to true (with republishToDlq=false ) causes the message to be re-queued and redelivered continually, which is likely not what you want unless the reason for the failure is transient. +In general, you should enable retry within the binder by setting maxAttempts to greater than one or by setting republishToDlq to true.

    See Section 41.3.1, “RabbitMQ Binder Properties” for more information about these properties.

    The framework does not provide any standard mechanism to consume dead-letter messages (or to re-route them back to the primary queue). +Some options are described in Section 41.6, “Dead-Letter Queue Processing”.

    [Note]Note

    When multiple RabbitMQ binders are used in a Spring Cloud Stream application, it is important to disable 'RabbitAutoConfiguration' to avoid the same configuration from RabbitAutoConfiguration being applied to the two binders. +You can exclude the class by using the @SpringBootApplication annotation.

    Starting with version 2.0, the RabbitMessageChannelBinder sets the RabbitTemplate.userPublisherConnection property to true so that the non-transactional producers avoid deadlocks on consumers, which can happen if cached connections are blocked because of a memory alarm on the broker.

    [Note]Note

    Currently, a multiplex consumer (a single consumer listening to multiple queues) is only supported for message-driven conssumers; polled consumers can only retrieve messages from a single queue.

    41.3 Configuration Options

    This section contains settings specific to the RabbitMQ Binder and bound channels.

    For general binding configuration options and properties, see the Spring Cloud Stream core documentation.

    41.3.1 RabbitMQ Binder Properties

    By default, the RabbitMQ binder uses Spring Boot’s ConnectionFactory. +Conseuqently, it supports all Spring Boot configuration options for RabbitMQ. +(For reference, see the Spring Boot documentation). +RabbitMQ configuration options use the spring.rabbitmq prefix.

    In addition to Spring Boot options, the RabbitMQ binder supports the following properties:

    spring.cloud.stream.rabbit.binder.adminAddresses

    A comma-separated list of RabbitMQ management plugin URLs. +Only used when nodes contains more than one entry. +Each entry in this list must have a corresponding entry in spring.rabbitmq.addresses. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See Queue Affinity and the LocalizedQueueConnectionFactory for more information.

    Default: empty.

    spring.cloud.stream.rabbit.binder.nodes

    A comma-separated list of RabbitMQ node names. +When more than one entry, used to locate the server address where a queue is located. +Each entry in this list must have a corresponding entry in spring.rabbitmq.addresses. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See Queue Affinity and the LocalizedQueueConnectionFactory for more information.

    Default: empty.

    spring.cloud.stream.rabbit.binder.compressionLevel

    The compression level for compressed bindings. +See java.util.zip.Deflater.

    Default: 1 (BEST_LEVEL).

    spring.cloud.stream.binder.connection-name-prefix

    A connection name prefix used to name the connection(s) created by this binder. +The name is this prefix followed by #n, where n increments each time a new connection is opened.

    Default: none (Spring AMQP default).

    41.3.2 RabbitMQ Consumer Properties

    The following properties are available for Rabbit consumers only and must be prefixed with spring.cloud.stream.rabbit.bindings.<channelName>.consumer..

    acknowledgeMode

    The acknowledge mode.

    Default: AUTO.

    autoBindDlq

    Whether to automatically declare the DLQ and bind it to the binder DLX.

    Default: false.

    bindingRoutingKey

    The routing key with which to bind the queue to the exchange (if bindQueue is true). +For partitioned destinations, -<instanceIndex> is appended.

    Default: #.

    bindQueue

    Whether to bind the queue to the destination exchange. +Set it to false if you have set up your own infrastructure and have previously created and bound the queue.

    Default: true.

    consumerTagPrefix

    Used to create the consumer tag(s); will be appended by #n where n increments for each consumer created. +Example: ${spring.application.name}-${spring.cloud.stream.bindings.input.group}-${spring.cloud.stream.instance-index}.

    Default: none - the broker will generate random consumer tags.

    deadLetterQueueName

    The name of the DLQ

    Default: prefix+destination.dlq

    deadLetterExchange

    A DLX to assign to the queue. +Relevant only if autoBindDlq is true.

    Default: 'prefix+DLX'

    deadLetterExchangeType

    The type of the DLX to assign to the queue. +Relevant only if autoBindDlq is true.

    Default: 'direct'

    deadLetterRoutingKey

    A dead letter routing key to assign to the queue. +Relevant only if autoBindDlq is true.

    Default: destination

    declareDlx

    Whether to declare the dead letter exchange for the destination. +Relevant only if autoBindDlq is true. +Set to false if you have a pre-configured DLX.

    Default: true.

    declareExchange

    Whether to declare the exchange for the destination.

    Default: true.

    delayedExchange

    Whether to declare the exchange as a Delayed Message Exchange. +Requires the delayed message exchange plugin on the broker. +The x-delayed-type argument is set to the exchangeType.

    Default: false.

    dlqDeadLetterExchange

    If a DLQ is declared, a DLX to assign to that queue.

    Default: none

    dlqDeadLetterRoutingKey

    If a DLQ is declared, a dead letter routing key to assign to that queue.

    Default: none

    dlqExpires

    How long before an unused dead letter queue is deleted (in milliseconds).

    Default: no expiration

    dlqLazy

    Declare the dead letter queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue.

    Default: false.

    dlqMaxLength

    Maximum number of messages in the dead letter queue.

    Default: no limit

    dlqMaxLengthBytes

    Maximum number of total bytes in the dead letter queue from all messages.

    Default: no limit

    dlqMaxPriority

    Maximum priority of messages in the dead letter queue (0-255).

    Default: none

    dlqOverflowBehavior

    Action to take when dlqMaxLength or dlqMaxLengthBytes is exceeded; currently drop-head or reject-publish but refer to the RabbitMQ documentation.

    Default: none

    dlqTtl

    Default time to live to apply to the dead letter queue when declared (in milliseconds).

    Default: no limit

    durableSubscription

    Whether the subscription should be durable. +Only effective if group is also set.

    Default: true.

    exchangeAutoDelete

    If declareExchange is true, whether the exchange should be auto-deleted (that is, removed after the last queue is removed).

    Default: true.

    exchangeDurable

    If declareExchange is true, whether the exchange should be durable (that is, it survives broker restart).

    Default: true.

    exchangeType

    The exchange type: direct, fanout or topic for non-partitioned destinations and direct or topic for partitioned destinations.

    Default: topic.

    exclusive

    Whether to create an exclusive consumer. +Concurrency should be 1 when this is true. +Often used when strict ordering is required but enabling a hot standby instance to take over after a failure. +See recoveryInterval, which controls how often a standby instance attempts to consume.

    Default: false.

    expires

    How long before an unused queue is deleted (in milliseconds).

    Default: no expiration

    failedDeclarationRetryInterval

    The interval (in milliseconds) between attempts to consume from a queue if it is missing.

    Default: 5000

    headerPatterns

    Patterns for headers to be mapped from inbound messages.

    Default: ['*'] (all headers).

    lazy

    Declare the queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue.

    Default: false.

    maxConcurrency

    The maximum number of consumers.

    Default: 1.

    maxLength

    The maximum number of messages in the queue.

    Default: no limit

    maxLengthBytes

    The maximum number of total bytes in the queue from all messages.

    Default: no limit

    maxPriority

    The maximum priority of messages in the queue (0-255).

    Default: none

    missingQueuesFatal

    When the queue cannot be found, whether to treat the condition as fatal and stop the listener container. +Defaults to false so that the container keeps trying to consume from the queue — for example, when using a cluster and the node hosting a non-HA queue is down.

    Default: false

    overflowBehavior

    Action to take when maxLength or maxLengthBytes is exceeded; currently drop-head or reject-publish but refer to the RabbitMQ documentation.

    Default: none

    prefetch

    Prefetch count.

    Default: 1.

    prefix

    A prefix to be added to the name of the destination and queues.

    Default: "".

    queueDeclarationRetries

    The number of times to retry consuming from a queue if it is missing. +Relevant only when missingQueuesFatal is true. +Otherwise, the container keeps retrying indefinitely.

    Default: 3

    queueNameGroupOnly

    When true, consume from a queue with a name equal to the group. +Otherwise the queue name is destination.group. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue.

    Default: false.

    recoveryInterval

    The interval between connection recovery attempts, in milliseconds.

    Default: 5000.

    requeueRejected

    Whether delivery failures should be re-queued when retry is disabled or republishToDlq is false.

    Default: false.

    republishDeliveryMode

    When republishToDlq is true, specifies the delivery mode of the republished message.

    Default: DeliveryMode.PERSISTENT

    republishToDlq

    By default, messages that fail after retries are exhausted are rejected. +If a dead-letter queue (DLQ) is configured, RabbitMQ routes the failed message (unchanged) to the DLQ. +If set to true, the binder republishs failed messages to the DLQ with additional headers, including the exception message and stack trace from the cause of the final failure.

    Default: false

    transacted

    Whether to use transacted channels.

    Default: false.

    ttl

    Default time to live to apply to the queue when declared (in milliseconds).

    Default: no limit

    txSize

    The number of deliveries between acks.

    Default: 1.

    41.3.3 Advanced Listener Container Configuration

    To set listener container properties that are not exposed as binder or binding properties, add a single bean of type ListenerContainerCustomizer to the application context. +The binder and binding properties will be set and then the customizer will be called. +The customizer (configure() method) is provided with the queue name as well as the consumer group as arguments.

    41.3.4 Rabbit Producer Properties

    The following properties are available for Rabbit producers only and +must be prefixed with spring.cloud.stream.rabbit.bindings.<channelName>.producer..

    autoBindDlq

    Whether to automatically declare the DLQ and bind it to the binder DLX.

    Default: false.

    batchingEnabled

    Whether to enable message batching by producers. +Messages are batched into one message according to the following properties (described in the next three entries in this list): 'batchSize', batchBufferLimit, and batchTimeout. +See Batching for more information.

    Default: false.

    batchSize

    The number of messages to buffer when batching is enabled.

    Default: 100.

    batchBufferLimit

    The maximum buffer size when batching is enabled.

    Default: 10000.

    batchTimeout

    The batch timeout when batching is enabled.

    Default: 5000.

    bindingRoutingKey

    The routing key with which to bind the queue to the exchange (if bindQueue is true). +Only applies to non-partitioned destinations. +Only applies if requiredGroups are provided and then only to those groups.

    Default: #.

    bindQueue

    Whether to bind the queue to the destination exchange. +Set it to false if you have set up your own infrastructure and have previously created and bound the queue. +Only applies if requiredGroups are provided and then only to those groups.

    Default: true.

    compress

    Whether data should be compressed when sent.

    Default: false.

    deadLetterQueueName

    The name of the DLQ +Only applies if requiredGroups are provided and then only to those groups.

    Default: prefix+destination.dlq

    deadLetterExchange

    A DLX to assign to the queue. +Relevant only when autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups.

    Default: 'prefix+DLX'

    deadLetterExchangeType

    The type of the DLX to assign to the queue. +Relevant only if autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups.

    Default: 'direct'

    deadLetterRoutingKey

    A dead letter routing key to assign to the queue. +Relevant only when autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups.

    Default: destination

    declareDlx

    Whether to declare the dead letter exchange for the destination. +Relevant only if autoBindDlq is true. +Set to false if you have a pre-configured DLX. +Applies only when requiredGroups are provided and then only to those groups.

    Default: true.

    declareExchange

    Whether to declare the exchange for the destination.

    Default: true.

    delayExpression

    A SpEL expression to evaluate the delay to apply to the message (x-delay header). +It has no effect if the exchange is not a delayed message exchange.

    Default: No x-delay header is set.

    delayedExchange

    Whether to declare the exchange as a Delayed Message Exchange. +Requires the delayed message exchange plugin on the broker. +The x-delayed-type argument is set to the exchangeType.

    Default: false.

    deliveryMode

    The delivery mode.

    Default: PERSISTENT.

    dlqDeadLetterExchange

    When a DLQ is declared, a DLX to assign to that queue. +Applies only if requiredGroups are provided and then only to those groups.

    Default: none

    dlqDeadLetterRoutingKey

    When a DLQ is declared, a dead letter routing key to assign to that queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: none

    dlqExpires

    How long (in milliseconds) before an unused dead letter queue is deleted. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no expiration

    dlqLazy
    Declare the dead letter queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when requiredGroups are provided and then only to those groups.
    dlqMaxLength

    Maximum number of messages in the dead letter queue. +Applies only if requiredGroups are provided and then only to those groups.

    Default: no limit

    dlqMaxLengthBytes

    Maximum number of total bytes in the dead letter queue from all messages. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    dlqMaxPriority

    Maximum priority of messages in the dead letter queue (0-255) +Applies only when requiredGroups are provided and then only to those groups.

    Default: none

    dlqTtl

    Default time (in milliseconds) to live to apply to the dead letter queue when declared. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    exchangeAutoDelete

    If declareExchange is true, whether the exchange should be auto-delete (it is removed after the last queue is removed).

    Default: true.

    exchangeDurable

    If declareExchange is true, whether the exchange should be durable (survives broker restart).

    Default: true.

    exchangeType

    The exchange type: direct, fanout or topic for non-partitioned destinations and direct or topic for partitioned destinations.

    Default: topic.

    expires

    How long (in milliseconds) before an unused queue is deleted. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no expiration

    headerPatterns

    Patterns for headers to be mapped to outbound messages.

    Default: ['*'] (all headers).

    lazy

    Declare the queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: false.

    maxLength

    Maximum number of messages in the queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    maxLengthBytes

    Maximum number of total bytes in the queue from all messages. +Only applies if requiredGroups are provided and then only to those groups.

    Default: no limit

    maxPriority

    Maximum priority of messages in the queue (0-255). +Only applies if requiredGroups are provided and then only to those groups.

    Default: none

    prefix

    A prefix to be added to the name of the destination exchange.

    Default: "".

    queueNameGroupOnly

    When true, consume from a queue with a name equal to the group. +Otherwise the queue name is destination.group. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: false.

    routingKeyExpression

    A SpEL expression to determine the routing key to use when publishing messages. +For a fixed routing key, use a literal expression, such as routingKeyExpression='my.routingKey' in a properties file or routingKeyExpression: '''my.routingKey''' in a YAML file.

    Default: destination or destination-<partition> for partitioned destinations.

    transacted

    Whether to use transacted channels.

    Default: false.

    ttl

    Default time (in milliseconds) to live to apply to the queue when declared. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    [Note]Note

    In the case of RabbitMQ, content type headers can be set by external applications. +Spring Cloud Stream supports them as part of an extended internal protocol used for any type of transport — including transports, such as Kafka (prior to 0.11), that do not natively support headers.

    41.4 Retry With the RabbitMQ Binder

    When retry is enabled within the binder, the listener container thread is suspended for any back off periods that are configured. +This might be important when strict ordering is required with a single consumer. However, for other use cases, it prevents other messages from being processed on that thread. +An alternative to using binder retry is to set up dead lettering with time to live on the dead-letter queue (DLQ) as well as dead-letter configuration on the DLQ itself. +See Section 41.3.1, “RabbitMQ Binder Properties” for more information about the properties discussed here. +You can use the following example configuration to enable this feature:

    • Set autoBindDlq to true. +The binder create a DLQ. +Optionally, you can specify a name in deadLetterQueueName.
    • Set dlqTtl to the back off time you want to wait between redeliveries.
    • Set the dlqDeadLetterExchange to the default exchange. +Expired messages from the DLQ are routed to the original queue, because the default deadLetterRoutingKey is the queue name (destination.group). +Setting to the default exchange is achieved by setting the property with no value, as shown in the next example.

    To force a message to be dead-lettered, either throw an AmqpRejectAndDontRequeueException or set requeueRejected to true (the default) and throw any exception.

    The loop continue without end, which is fine for transient problems, but you may want to give up after some number of attempts. +Fortunately, RabbitMQ provides the x-death header, which lets you determine how many cycles have occurred.

    To acknowledge a message after giving up, throw an ImmediateAcknowledgeAmqpException.

    41.4.1 Putting it All Together

    The following configuration creates an exchange myDestination with queue myDestination.consumerGroup bound to a topic exchange with a wildcard routing key #:

    ---
    +spring.cloud.stream.bindings.input.destination=myDestination
    +spring.cloud.stream.bindings.input.group=consumerGroup
    +#disable binder retries
    +spring.cloud.stream.bindings.input.consumer.max-attempts=1
    +#dlx/dlq setup
    +spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true
    +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-ttl=5000
    +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-dead-letter-exchange=
    +---

    This configuration creates a DLQ bound to a direct exchange (DLX) with a routing key of myDestination.consumerGroup. +When messages are rejected, they are routed to the DLQ. +After 5 seconds, the message expires and is routed to the original queue by using the queue name as the routing key, as shown in the following example:

    Spring Boot application.  +

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class XDeathApplication {
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(XDeathApplication.class, args);
    +    }
    +
    +    @StreamListener(Sink.INPUT)
    +    public void listen(String in, @Header(name = "x-death", required = false) Map<?,?> death) {
    +        if (death != null && death.get("count").equals(3L)) {
    +            // giving up - don't send to DLX
    +            throw new ImmediateAcknowledgeAmqpException("Failed after 4 attempts");
    +        }
    +        throw new AmqpRejectAndDontRequeueException("failed");
    +    }
    +
    +}

    +

    Notice that the count property in the x-death header is a Long.

    41.5 Error Channels

    Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See ??? for more information.

    RabbitMQ has two types of send failures:

    The latter is rare. +According to the RabbitMQ documentation "[A nack] will only be delivered if an internal error occurs in the Erlang process responsible for a queue.".

    As well as enabling producer error channels (as described in ???), the RabbitMQ binder only sends messages to the channels if the connection factory is appropriately configured, as follows:

    • ccf.setPublisherConfirms(true);
    • ccf.setPublisherReturns(true);

    When using Spring Boot configuration for the connection factory, set the following properties:

    • spring.rabbitmq.publisher-confirms
    • spring.rabbitmq.publisher-returns

    The payload of the ErrorMessage for a returned message is a ReturnedAmqpMessageException with the following properties:

    • failedMessage: The spring-messaging Message<?> that failed to be sent.
    • amqpMessage: The raw spring-amqp Message.
    • replyCode: An integer value indicating the reason for the failure (for example, 312 - No route).
    • replyText: A text value indicating the reason for the failure (for example, NO_ROUTE).
    • exchange: The exchange to which the message was published.
    • routingKey: The routing key used when the message was published.

    For negatively acknowledged confirmations, the payload is a NackedAmqpMessageException with the following properties:

    • failedMessage: The spring-messaging Message<?> that failed to be sent.
    • nackReason: A reason (if available — you may need to examine the broker logs for more information).

    There is no automatic handling of these exceptions (such as sending to a dead-letter queue). +You can consume these exceptions with your own Spring Integration flow.

    41.6 Dead-Letter Queue Processing

    Because you cannot anticipate how users would want to dispose of dead-lettered messages, the framework does not provide any standard mechanism to handle them. +If the reason for the dead-lettering is transient, you may wish to route the messages back to the original queue. +However, if the problem is a permanent issue, that could cause an infinite loop. +The following Spring Boot application shows an example of how to route those messages back to the original queue but moves them to a third parking lot queue after three attempts. +The second example uses the RabbitMQ Delayed Message Exchange to introduce a delay to the re-queued message. +In this example, the delay increases for each attempt. +These examples use a @RabbitListener to receive messages from the DLQ. +You could also use RabbitTemplate.receive() in a batch process.

    The examples assume the original destination is so8400in and the consumer group is so8400.

    41.6.1 Non-Partitioned Destinations

    The first two examples are for when the destination is not partitioned:

    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +    private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +    private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +    private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +    private static final String X_RETRIES_HEADER = "x-retries";
    +
    +    public static void main(String[] args) throws Exception {
    +        ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +        System.out.println("Hit enter to terminate");
    +        System.in.read();
    +        context.close();
    +    }
    +
    +    @Autowired
    +    private RabbitTemplate rabbitTemplate;
    +
    +    @RabbitListener(queues = DLQ)
    +    public void rePublish(Message failedMessage) {
    +        Integer retriesHeader = (Integer) failedMessage.getMessageProperties().getHeaders().get(X_RETRIES_HEADER);
    +        if (retriesHeader == null) {
    +            retriesHeader = Integer.valueOf(0);
    +        }
    +        if (retriesHeader < 3) {
    +            failedMessage.getMessageProperties().getHeaders().put(X_RETRIES_HEADER, retriesHeader + 1);
    +            this.rabbitTemplate.send(ORIGINAL_QUEUE, failedMessage);
    +        }
    +        else {
    +            this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +        }
    +    }
    +
    +    @Bean
    +    public Queue parkingLot() {
    +        return new Queue(PARKING_LOT);
    +    }
    +
    +}
    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +    private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +    private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +    private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +    private static final String X_RETRIES_HEADER = "x-retries";
    +
    +    private static final String DELAY_EXCHANGE = "dlqReRouter";
    +
    +    public static void main(String[] args) throws Exception {
    +        ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +        System.out.println("Hit enter to terminate");
    +        System.in.read();
    +        context.close();
    +    }
    +
    +    @Autowired
    +    private RabbitTemplate rabbitTemplate;
    +
    +    @RabbitListener(queues = DLQ)
    +    public void rePublish(Message failedMessage) {
    +        Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders();
    +        Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER);
    +        if (retriesHeader == null) {
    +            retriesHeader = Integer.valueOf(0);
    +        }
    +        if (retriesHeader < 3) {
    +            headers.put(X_RETRIES_HEADER, retriesHeader + 1);
    +            headers.put("x-delay", 5000 * retriesHeader);
    +            this.rabbitTemplate.send(DELAY_EXCHANGE, ORIGINAL_QUEUE, failedMessage);
    +        }
    +        else {
    +            this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +        }
    +    }
    +
    +    @Bean
    +    public DirectExchange delayExchange() {
    +        DirectExchange exchange = new DirectExchange(DELAY_EXCHANGE);
    +        exchange.setDelayed(true);
    +        return exchange;
    +    }
    +
    +    @Bean
    +    public Binding bindOriginalToDelay() {
    +        return BindingBuilder.bind(new Queue(ORIGINAL_QUEUE)).to(delayExchange()).with(ORIGINAL_QUEUE);
    +    }
    +
    +    @Bean
    +    public Queue parkingLot() {
    +        return new Queue(PARKING_LOT);
    +    }
    +
    +}

    41.6.2 Partitioned Destinations

    With partitioned destinations, there is one DLQ for all partitions. We determine the original queue from the headers.

    republishToDlq=false

    When republishToDlq is false, RabbitMQ publishes the message to the DLX/DLQ with an x-death header containing information about the original destination, as shown in the following example:

    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +	private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +	private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +	private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +	private static final String X_DEATH_HEADER = "x-death";
    +
    +	private static final String X_RETRIES_HEADER = "x-retries";
    +
    +	public static void main(String[] args) throws Exception {
    +		ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +		System.out.println("Hit enter to terminate");
    +		System.in.read();
    +		context.close();
    +	}
    +
    +	@Autowired
    +	private RabbitTemplate rabbitTemplate;
    +
    +	@SuppressWarnings("unchecked")
    +	@RabbitListener(queues = DLQ)
    +	public void rePublish(Message failedMessage) {
    +		Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders();
    +		Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER);
    +		if (retriesHeader == null) {
    +			retriesHeader = Integer.valueOf(0);
    +		}
    +		if (retriesHeader < 3) {
    +			headers.put(X_RETRIES_HEADER, retriesHeader + 1);
    +			List<Map<String, ?>> xDeath = (List<Map<String, ?>>) headers.get(X_DEATH_HEADER);
    +			String exchange = (String) xDeath.get(0).get("exchange");
    +			List<String> routingKeys = (List<String>) xDeath.get(0).get("routing-keys");
    +			this.rabbitTemplate.send(exchange, routingKeys.get(0), failedMessage);
    +		}
    +		else {
    +			this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +		}
    +	}
    +
    +	@Bean
    +	public Queue parkingLot() {
    +		return new Queue(PARKING_LOT);
    +	}
    +
    +}

    republishToDlq=true

    When republishToDlq is true, the republishing recoverer adds the original exchange and routing key to headers, as shown in the following example:

    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +	private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +	private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +	private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +	private static final String X_RETRIES_HEADER = "x-retries";
    +
    +	private static final String X_ORIGINAL_EXCHANGE_HEADER = RepublishMessageRecoverer.X_ORIGINAL_EXCHANGE;
    +
    +	private static final String X_ORIGINAL_ROUTING_KEY_HEADER = RepublishMessageRecoverer.X_ORIGINAL_ROUTING_KEY;
    +
    +	public static void main(String[] args) throws Exception {
    +		ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +		System.out.println("Hit enter to terminate");
    +		System.in.read();
    +		context.close();
    +	}
    +
    +	@Autowired
    +	private RabbitTemplate rabbitTemplate;
    +
    +	@RabbitListener(queues = DLQ)
    +	public void rePublish(Message failedMessage) {
    +		Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders();
    +		Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER);
    +		if (retriesHeader == null) {
    +			retriesHeader = Integer.valueOf(0);
    +		}
    +		if (retriesHeader < 3) {
    +			headers.put(X_RETRIES_HEADER, retriesHeader + 1);
    +			String exchange = (String) headers.get(X_ORIGINAL_EXCHANGE_HEADER);
    +			String originalRoutingKey = (String) headers.get(X_ORIGINAL_ROUTING_KEY_HEADER);
    +			this.rabbitTemplate.send(exchange, originalRoutingKey, failedMessage);
    +		}
    +		else {
    +			this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +		}
    +	}
    +
    +	@Bean
    +	public Queue parkingLot() {
    +		return new Queue(PARKING_LOT);
    +	}
    +
    +}

    41.7 Partitioning with the RabbitMQ Binder

    RabbitMQ does not support partitioning natively.

    Sometimes, it is advantageous to send data to specific partitions — for example, when you want to strictly order message processing, all messages for a particular customer should go to the same partition.

    The RabbitMessageChannelBinder provides partitioning by binding a queue for each partition to the destination exchange.

    The following Java and YAML examples show how to configure the producer:

    Producer.  +

    @SpringBootApplication
    +@EnableBinding(Source.class)
    +public class RabbitPartitionProducerApplication {
    +
    +    private static final Random RANDOM = new Random(System.currentTimeMillis());
    +
    +    private static final String[] data = new String[] {
    +            "abc1", "def1", "qux1",
    +            "abc2", "def2", "qux2",
    +            "abc3", "def3", "qux3",
    +            "abc4", "def4", "qux4",
    +            };
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(RabbitPartitionProducerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedRate = "5000"))
    +    public Message<?> generate() {
    +        String value = data[RANDOM.nextInt(data.length)];
    +        System.out.println("Sending: " + value);
    +        return MessageBuilder.withPayload(value)
    +                .setHeader("partitionKey", value)
    +                .build();
    +    }
    +
    +}

    +

    application.yml.  +

        spring:
    +      cloud:
    +        stream:
    +          bindings:
    +            output:
    +              destination: partitioned.destination
    +              producer:
    +                partitioned: true
    +                partition-key-expression: headers['partitionKey']
    +                partition-count: 2
    +                required-groups:
    +                - myGroup

    +

    [Note]Note

    The configuration in the prececing example uses the default partitioning (key.hashCode() % partitionCount). +This may or may not provide a suitably balanced algorithm, depending on the key values. +You can override this default by using the partitionSelectorExpression or partitionSelectorClass properties.

    The required-groups property is required only if you need the consumer queues to be provisioned when the producer is deployed. +Otherwise, any messages sent to a partition are lost until the corresponding consumer is deployed.

    The following configuration provisions a topic exchange:

    part exchange

    The following queues are bound to that exchange:

    part queues

    The following bindings associate the queues to the exchange:

    part bindings

    The following Java and YAML examples continue the previous examples and show how to configure the consumer:

    Consumer.  +

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class RabbitPartitionConsumerApplication {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(RabbitPartitionConsumerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @StreamListener(Sink.INPUT)
    +    public void listen(@Payload String in, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) {
    +        System.out.println(in + " received from queue " + queue);
    +    }
    +
    +}

    +

    application.yml.  +

        spring:
    +      cloud:
    +        stream:
    +          bindings:
    +            input:
    +              destination: partitioned.destination
    +              group: myGroup
    +              consumer:
    +                partitioned: true
    +                instance-index: 0

    +

    [Important]Important

    The RabbitMessageChannelBinder does not support dynamic scaling. +There must be at least one consumer per partition. +The consumer’s instanceIndex is used to indicate which partition is consumed. +Platforms such as Cloud Foundry can have only one instance with an instanceIndex.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__reactor_netty_access_logs.html b/Greenwich.SR5/multi/multi__reactor_netty_access_logs.html new file mode 100644 index 00000000..dc295aef --- /dev/null +++ b/Greenwich.SR5/multi/multi__reactor_netty_access_logs.html @@ -0,0 +1,17 @@ + + + 120. Reactor Netty Access Logs

    120. Reactor Netty Access Logs

    To enable Reactor Netty access logs, set -Dreactor.netty.http.server.accessLogEnabled=true. (It must be a Java System Property, not a Spring Boot property).

    The logging system can be configured to have a separate access log file. Below is an example logback configuration:

    logback.xml.  +

        <appender name="accessLog" class="ch.qos.logback.core.FileAppender">
    +        <file>access_log.log</file>
    +        <encoder>
    +            <pattern>%msg%n</pattern>
    +        </encoder>
    +    </appender>
    +    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    +        <appender-ref ref="accessLog" />
    +    </appender>
    +
    +    <logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
    +        <appender-ref ref="async"/>
    +    </logger>

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__ribbon_discovery_in_kubernetes.html b/Greenwich.SR5/multi/multi__ribbon_discovery_in_kubernetes.html new file mode 100644 index 00000000..4b37031e --- /dev/null +++ b/Greenwich.SR5/multi/multi__ribbon_discovery_in_kubernetes.html @@ -0,0 +1,20 @@ + + + 141. Ribbon Discovery in Kubernetes

    141. Ribbon Discovery in Kubernetes

    Spring Cloud client applications that call a microservice should be interested on relying on a client load-balancing +feature in order to automatically discover at which endpoint(s) it can reach a given service. This mechanism has been +implemented within the spring-cloud-kubernetes-ribbon project, where a +Kubernetes client populates a Ribbon ServerList that contains information +about such endpoints.

    The implementation is part of the following starter that you can use by adding its dependency to your pom file:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
    +    <version>${latest.version}</version>
    +</dependency>

    When the list of the endpoints is populated, the Kubernetes client searches the registered endpoints that live in +the current namespace or project by matching the service name defined in the Ribbon Client annotation, as follows:

    @RibbonClient(name = "name-service")

    You can configure Ribbon’s behavior by providing properties in your application.properties (through your application’s +dedicated ConfigMap) by using the following format: <name of your service>.ribbon.<Ribbon configuration key>, where:

    • <name of your service> corresponds to the service name you access over Ribbon, as configured by using the +@RibbonClient annotation (such as name-service in the preceding example).
    • <Ribbon configuration key> is one of the Ribbon configuration keys defined by +Ribbon’s CommonClientConfigKey class.

    Additionally, the spring-cloud-kubernetes-ribbon project defines two additional configuration keys to further +control how Ribbon interacts with Kubernetes. In particular, if an endpoint defines multiple ports, the default +behavior is to use the first one found. To select more specifically which port to use in a multi-port service, you can use +the PortName key. If you want to specify in which Kubernetes namespace the target service should be looked up, you can use +the KubernetesNamespace key, remembering in both instances to prefix these keys with your service name and +ribbon prefix, as specified earlier.

    The following examples use this module for ribbon discovery:

    [Note]Note

    You can disable the Ribbon discovery client by setting the spring.cloud.kubernetes.ribbon.enabled=false key within the application properties file.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__router_and_filter_zuul.html b/Greenwich.SR5/multi/multi__router_and_filter_zuul.html new file mode 100644 index 00000000..0a0f43ee --- /dev/null +++ b/Greenwich.SR5/multi/multi__router_and_filter_zuul.html @@ -0,0 +1,494 @@ + + + 18. Router and Filter: Zuul

    18. Router and Filter: Zuul

    Routing is an integral part of a microservice architecture. +For example, / may be mapped to your web application, /api/users is mapped to the user service and /api/shop is mapped to the shop service. +Zuul is a JVM-based router and server-side load balancer from Netflix.

    Netflix uses Zuul for the following:

    • Authentication
    • Insights
    • Stress Testing
    • Canary Testing
    • Dynamic Routing
    • Service Migration
    • Load Shedding
    • Security
    • Static Response handling
    • Active/Active traffic management

    Zuul’s rule engine lets rules and filters be written in essentially any JVM language, with built-in support for Java and Groovy.

    [Note]Note

    The configuration property zuul.max.host.connections has been replaced by two new properties, zuul.host.maxTotalConnections and zuul.host.maxPerRouteConnections, which default to 200 and 20 respectively.

    [Note]Note

    The default Hystrix isolation pattern (ExecutionIsolationStrategy) for all routes is SEMAPHORE. +zuul.ribbonIsolationStrategy can be changed to THREAD if that isolation pattern is preferred.

    18.1 How to Include Zuul

    To include Zuul in your project, use the starter with a group ID of org.springframework.cloud and a artifact ID of spring-cloud-starter-netflix-zuul. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    18.2 Embedded Zuul Reverse Proxy

    Spring Cloud has created an embedded Zuul proxy to ease the development of a common use case where a UI application wants to make proxy calls to one or more back end services. +This feature is useful for a user interface to proxy to the back end services it requires, avoiding the need to manage CORS and authentication concerns independently for all the back ends.

    To enable it, annotate a Spring Boot main class with @EnableZuulProxy. Doing so causes local calls to be forwarded to the appropriate service. +By convention, a service with an ID of users receives requests from the proxy located at /users (with the prefix stripped). +The proxy uses Ribbon to locate an instance to which to forward through discovery. +All requests are executed in a hystrix command, so failures appear in Hystrix metrics. +Once the circuit is open, the proxy does not try to contact the service.

    [Note]Note

    the Zuul starter does not include a discovery client, so, for routes based on service IDs, you need to provide one of those on the classpath as well (Eureka is one choice).

    To skip having a service automatically added, set zuul.ignored-services to a list of service ID patterns. +If a service matches a pattern that is ignored but is also included in the explicitly configured routes map, it is unignored, as shown in the following example:

    application.yml.  +

     zuul:
    +  ignoredServices: '*'
    +  routes:
    +    users: /myusers/**

    +

    In the preceding example, all services are ignored, except for users.

    To augment or change the proxy routes, you can add external configuration, as follows:

    application.yml.  +

     zuul:
    +  routes:
    +    users: /myusers/**

    +

    The preceding example means that HTTP calls to /myusers get forwarded to the users service (for example /myusers/101 is forwarded to /101).

    To get more fine-grained control over a route, you can specify the path and the serviceId independently, as follows:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      serviceId: users_service

    +

    The preceding example means that HTTP calls to /myusers get forwarded to the users_service service. +The route must have a path that can be specified as an ant-style pattern, so /myusers/* only matches one level, but /myusers/** matches hierarchically.

    The location of the back end can be specified as either a serviceId (for a service from discovery) or a url (for a physical location), as shown in the following example:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      url: https://example.com/users_service

    +

    These simple url-routes do not get executed as a HystrixCommand, nor do they load-balance multiple URLs with Ribbon. +To achieve those goals, you can specify a serviceId with a static list of servers, as follows:

    application.yml.  +

    zuul:
    +  routes:
    +    echo:
    +      path: /myusers/**
    +      serviceId: myusers-service
    +      stripPrefix: true
    +
    +hystrix:
    +  command:
    +    myusers-service:
    +      execution:
    +        isolation:
    +          thread:
    +            timeoutInMilliseconds: ...
    +
    +myusers-service:
    +  ribbon:
    +    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    +    listOfServers: https://example1.com,http://example2.com
    +    ConnectTimeout: 1000
    +    ReadTimeout: 3000
    +    MaxTotalHttpConnections: 500
    +    MaxConnectionsPerHost: 100

    +

    Another method is specifiying a service-route and configuring a Ribbon client for the serviceId (doing so requires disabling Eureka support in Ribbon — see above for more information), as shown in the following example:

    application.yml.  +

    zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      serviceId: users
    +
    +ribbon:
    +  eureka:
    +    enabled: false
    +
    +users:
    +  ribbon:
    +    listOfServers: example.com,google.com

    +

    You can provide a convention between serviceId and routes by using regexmapper. +It uses regular-expression named groups to extract variables from serviceId and inject them into a route pattern, as shown in the following example:

    ApplicationConfiguration.java.  +

    @Bean
    +public PatternServiceRouteMapper serviceRouteMapper() {
    +    return new PatternServiceRouteMapper(
    +        "(?<name>^.+)-(?<version>v.+$)",
    +        "${version}/${name}");
    +}

    +

    The preceding example means that a serviceId of myusers-v1 is mapped to route /v1/myusers/**. +Any regular expression is accepted, but all named groups must be present in both servicePattern and routePattern. +If servicePattern does not match a serviceId, the default behavior is used. +In the preceding example, a serviceId of myusers is mapped to the "/myusers/**" route (with no version detected). +This feature is disabled by default and only applies to discovered services.

    To add a prefix to all mappings, set zuul.prefix to a value, such as /api. +By default, the proxy prefix is stripped from the request before the request is forwarded by (you can switch this behavior off with zuul.stripPrefix=false). +You can also switch off the stripping of the service-specific prefix from individual routes, as shown in the following example:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      stripPrefix: false

    +

    [Note]Note

    zuul.stripPrefix only applies to the prefix set in zuul.prefix. +It does not have any effect on prefixes defined within a given route’s path.

    In the preceding example, requests to /myusers/101 are forwarded to /myusers/101 on the users service.

    The zuul.routes entries actually bind to an object of type ZuulProperties. +If you look at the properties of that object, you can see that it also has a retryable flag. +Set that flag to true to have the Ribbon client automatically retry failed requests. +You can also set that flag to true when you need to modify the parameters of the retry operations that use the Ribbon client configuration.

    By default, the X-Forwarded-Host header is added to the forwarded requests. +To turn it off, set zuul.addProxyHeaders = false. +By default, the prefix path is stripped, and the request to the back end picks up a X-Forwarded-Prefix header (/myusers in the examples shown earlier).

    If you set a default route (/), an application with @EnableZuulProxy could act as a standalone server. For example, zuul.route.home: / would route all traffic ("/**") to the "home" service.

    If more fine-grained ignoring is needed, you can specify specific patterns to ignore. +These patterns are evaluated at the start of the route location process, which means prefixes should be included in the pattern to warrant a match. +Ignored patterns span all services and supersede any other route specification. +The following example shows how to create ignored patterns:

    application.yml.  +

     zuul:
    +  ignoredPatterns: /**/admin/**
    +  routes:
    +    users: /myusers/**

    +

    The preceding example means that all calls (such as /myusers/101) are forwarded to /101 on the users service. +However, calls including /admin/ do not resolve.

    [Warning]Warning

    If you need your routes to have their order preserved, you need to use a YAML file, as the ordering is lost when using a properties file. +The following example shows such a YAML file:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +    legacy:
    +      path: /**

    +

    If you were to use a properties file, the legacy path might end up in front of the users +path, rendering the users path unreachable.

    18.3 Zuul Http Client

    The default HTTP client used by Zuul is now backed by the Apache HTTP Client instead of the deprecated Ribbon RestClient. +To use RestClient or okhttp3.OkHttpClient, set ribbon.restclient.enabled=true or ribbon.okhttp.enabled=true, respectively. +If you would like to customize the Apache HTTP client or the OK HTTP client, provide a bean of type ClosableHttpClient or OkHttpClient.

    18.4 Cookies and Sensitive Headers

    You can share headers between services in the same system, but you probably do not want sensitive headers leaking downstream into external servers. +You can specify a list of ignored headers as part of the route configuration. +Cookies play a special role, because they have well defined semantics in browsers, and they are always to be treated as sensitive. +If the consumer of your proxy is a browser, then cookies for downstream services also cause problems for the user, because they all get jumbled up together (all downstream services look like they come from the same place).

    If you are careful with the design of your services, (for example, if only one of the downstream services sets cookies), you might be able to let them flow from the back end all the way up to the caller. +Also, if your proxy sets cookies and all your back-end services are part of the same system, it can be natural to simply share them (and, for instance, use Spring Session to link them up to some shared state). +Other than that, any cookies that get set by downstream services are likely to be not useful to the caller, so it is recommended that you make (at least) Set-Cookie and Cookie into sensitive headers for routes that are not part of your domain. +Even for routes that are part of your domain, try to think carefully about what it means before letting cookies flow between them and the proxy.

    The sensitive headers can be configured as a comma-separated list per route, as shown in the following example:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      sensitiveHeaders: Cookie,Set-Cookie,Authorization
    +      url: https://downstream

    +

    [Note]Note

    This is the default value for sensitiveHeaders, so you need not set it unless you want it to be different. +This is new in Spring Cloud Netflix 1.1 (in 1.0, the user had no control over headers, and all cookies flowed in both directions).

    The sensitiveHeaders are a blacklist, and the default is not empty. +Consequently, to make Zuul send all headers (except the ignored ones), you must explicitly set it to the empty list. +Doing so is necessary if you want to pass cookie or authorization headers to your back end. The following example shows how to use sensitiveHeaders:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      sensitiveHeaders:
    +      url: https://downstream

    +

    You can also set sensitive headers, by setting zuul.sensitiveHeaders. +If sensitiveHeaders is set on a route, it overrides the global sensitiveHeaders setting.

    18.5 Ignored Headers

    In addition to the route-sensitive headers, you can set a global value called zuul.ignoredHeaders for values (both request and response) that should be discarded during interactions with downstream services. +By default, if Spring Security is not on the classpath, these are empty. +Otherwise, they are initialized to a set of well known security headers (for example, involving caching) as specified by Spring Security. +The assumption in this case is that the downstream services might add these headers, too, but we want the values from the proxy. +To not discard these well known security headers when Spring Security is on the classpath, you can set zuul.ignoreSecurityHeaders to false. +Doing so can be useful if you disabled the HTTP Security response headers in Spring Security and want the values provided by downstream services.

    18.6 Management Endpoints

    By default, if you use @EnableZuulProxy with the Spring Boot Actuator, you enable two additional endpoints:

    • Routes
    • Filters

    18.6.1 Routes Endpoint

    A GET to the routes endpoint at /routes returns a list of the mapped routes:

    GET /routes.  +

    {
    +  /stores/**: "http://localhost:8081"
    +}

    +

    Additional route details can be requested by adding the ?format=details query string to /routes. +Doing so produces the following output:

    GET /routes/details.  +

    {
    +  "/stores/**": {
    +    "id": "stores",
    +    "fullPath": "/stores/**",
    +    "location": "http://localhost:8081",
    +    "path": "/**",
    +    "prefix": "/stores",
    +    "retryable": false,
    +    "customSensitiveHeaders": false,
    +    "prefixStripped": true
    +  }
    +}

    +

    A POST to /routes forces a refresh of the existing routes (for example, when there have been changes in the service catalog). +You can disable this endpoint by setting endpoints.routes.enabled to false.

    [Note]Note

    the routes should respond automatically to changes in the service catalog, but the POST to /routes is a way to force the change +to happen immediately.

    18.6.2 Filters Endpoint

    A GET to the filters endpoint at /filters returns a map of Zuul filters by type. +For each filter type in the map, you get a list of all the filters of that type, along with their details.

    18.7 Strangulation Patterns and Local Forwards

    A common pattern when migrating an existing application or API is to strangle old endpoints, slowly replacing them with different implementations. +The Zuul proxy is a useful tool for this because you can use it to handle all traffic from the clients of the old endpoints but redirect some of the requests to new ones.

    The following example shows the configuration details for a strangle scenario:

    application.yml.  +

     zuul:
    +  routes:
    +    first:
    +      path: /first/**
    +      url: https://first.example.com
    +    second:
    +      path: /second/**
    +      url: forward:/second
    +    third:
    +      path: /third/**
    +      url: forward:/3rd
    +    legacy:
    +      path: /**
    +      url: https://legacy.example.com

    +

    In the preceding example, we are strangle the legacy application, which is mapped to all requests that do not match one of the other patterns. +Paths in /first/** have been extracted into a new service with an external URL. +Paths in /second/** are forwarded so that they can be handled locally (for example, with a normal Spring @RequestMapping). +Paths in /third/** are also forwarded but with a different prefix (/third/foo is forwarded to /3rd/foo).

    [Note]Note

    The ignored patterns aren’t completely ignored, they just are not handled by the proxy (so they are also effectively forwarded locally).

    18.8 Uploading Files through Zuul

    If you use @EnableZuulProxy, you can use the proxy paths to upload files and it should work, so long as the files are small. +For large files there is an alternative path that bypasses the Spring DispatcherServlet (to avoid multipart processing) in "/zuul/*". +In other words, if you have zuul.routes.customers=/customers/**, then you can POST large files to /zuul/customers/*. +The servlet path is externalized via zuul.servletPath. +If the proxy route takes you through a Ribbon load balancer, extremely large files also require elevated timeout settings, as shown in the following example:

    application.yml.  +

    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
    +ribbon:
    +  ConnectTimeout: 3000
    +  ReadTimeout: 60000

    +

    Note that, for streaming to work with large files, you need to use chunked encoding in the request (which some browsers do not do by default), as shown in the following example:

    $ curl -v -H "Transfer-Encoding: chunked" \
    +    -F "file=@mylarge.iso" localhost:9999/zuul/simple/file

    18.9 Query String Encoding

    When processing the incoming request, query params are decoded so that they can be available for possible modifications in Zuul filters. +They are then re-encoded the back end request is rebuilt in the route filters. +The result can be different than the original input if (for example) it was encoded with Javascript’s encodeURIComponent() method. +While this causes no issues in most cases, some web servers can be picky with the encoding of complex query string.

    To force the original encoding of the query string, it is possible to pass a special flag to ZuulProperties so that the query string is taken as is with the HttpServletRequest::getQueryString method, as shown in the following example:

    application.yml.  +

     zuul:
    +  forceOriginalQueryStringEncoding: true

    +

    [Note]Note

    This special flag works only with SimpleHostRoutingFilter. Also, you loose the ability to easily override +query parameters with RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters), because +the query string is now fetched directly on the original HttpServletRequest.

    18.10 Request URI Encoding

    When processing the incoming request, request URI is decoded before matching them to routes. +The request URI is then re-encoded when the back end request is rebuilt in the route filters. +This can cause some unexpected behavior if your URI includes the encoded "/" character.

    To use the original request URI, it is possible to pass a special flag to 'ZuulProperties' so that the URI will be taken as is with the HttpServletRequest::getRequestURI method, as shown in the following example:

    application.yml.  +

     zuul:
    +  decodeUrl: false

    +

    [Note]Note

    If you are overriding request URI using requestURI RequestContext attribute and this flag is set to false, then the URL set in the request context will not be encoded. It will be your responsibility to make sure the URL is already encoded.

    18.11 Plain Embedded Zuul

    If you use @EnableZuulServer (instead of @EnableZuulProxy), you can also run a Zuul server without proxying or selectively switch on parts of the proxying platform. +Any beans that you add to the application of type ZuulFilter are installed automatically (as they are with @EnableZuulProxy) but without any of the proxy filters being added automatically.

    In that case, the routes into the Zuul server are still specified by configuring "zuul.routes.*", but there is no service discovery and no proxying. Consequently, the "serviceId" and "url" settings are ignored. +The following example maps all paths in "/api/**" to the Zuul filter chain:

    application.yml.  +

     zuul:
    +  routes:
    +    api: /api/**

    +

    18.12 Disable Zuul Filters

    Zuul for Spring Cloud comes with a number of ZuulFilter beans enabled by default in both proxy and server mode. +See the Zuul filters package for the list of filters that you can enable. +If you want to disable one, set zuul.<SimpleClassName>.<filterType>.disable=true. +By convention, the package after filters is the Zuul filter type. +For example to disable org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter, set zuul.SendResponseFilter.post.disable=true.

    18.13 Providing Hystrix Fallbacks For Routes

    When a circuit for a given route in Zuul is tripped, you can provide a fallback response by creating a bean of type FallbackProvider. +Within this bean, you need to specify the route ID the fallback is for and provide a ClientHttpResponse to return as a fallback. +The following example shows a relatively simple FallbackProvider implementation:

    class MyFallbackProvider implements FallbackProvider {
    +
    +    @Override
    +    public String getRoute() {
    +        return "customers";
    +    }
    +
    +    @Override
    +    public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
    +        if (cause instanceof HystrixTimeoutException) {
    +            return response(HttpStatus.GATEWAY_TIMEOUT);
    +        } else {
    +            return response(HttpStatus.INTERNAL_SERVER_ERROR);
    +        }
    +    }
    +
    +    private ClientHttpResponse response(final HttpStatus status) {
    +        return new ClientHttpResponse() {
    +            @Override
    +            public HttpStatus getStatusCode() throws IOException {
    +                return status;
    +            }
    +
    +            @Override
    +            public int getRawStatusCode() throws IOException {
    +                return status.value();
    +            }
    +
    +            @Override
    +            public String getStatusText() throws IOException {
    +                return status.getReasonPhrase();
    +            }
    +
    +            @Override
    +            public void close() {
    +            }
    +
    +            @Override
    +            public InputStream getBody() throws IOException {
    +                return new ByteArrayInputStream("fallback".getBytes());
    +            }
    +
    +            @Override
    +            public HttpHeaders getHeaders() {
    +                HttpHeaders headers = new HttpHeaders();
    +                headers.setContentType(MediaType.APPLICATION_JSON);
    +                return headers;
    +            }
    +        };
    +    }
    +}

    The following example shows how the route configuration for the previous example might appear:

    zuul:
    +  routes:
    +    customers: /customers/**

    If you would like to provide a default fallback for all routes, you can create a bean of type FallbackProvider and have the getRoute method return * or null, as shown in the following example:

    class MyFallbackProvider implements FallbackProvider {
    +    @Override
    +    public String getRoute() {
    +        return "*";
    +    }
    +
    +    @Override
    +    public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
    +        return new ClientHttpResponse() {
    +            @Override
    +            public HttpStatus getStatusCode() throws IOException {
    +                return HttpStatus.OK;
    +            }
    +
    +            @Override
    +            public int getRawStatusCode() throws IOException {
    +                return 200;
    +            }
    +
    +            @Override
    +            public String getStatusText() throws IOException {
    +                return "OK";
    +            }
    +
    +            @Override
    +            public void close() {
    +
    +            }
    +
    +            @Override
    +            public InputStream getBody() throws IOException {
    +                return new ByteArrayInputStream("fallback".getBytes());
    +            }
    +
    +            @Override
    +            public HttpHeaders getHeaders() {
    +                HttpHeaders headers = new HttpHeaders();
    +                headers.setContentType(MediaType.APPLICATION_JSON);
    +                return headers;
    +            }
    +        };
    +    }
    +}

    18.14 Zuul Timeouts

    If you want to configure the socket timeouts and read timeouts for requests proxied through Zuul, you have two options, based on your configuration:

    • If Zuul uses service discovery, you need to configure these timeouts with the +ribbon.ReadTimeout and ribbon.SocketTimeout Ribbon properties.

    If you have configured Zuul routes by specifying URLs, you need to use +zuul.host.connect-timeout-millis and zuul.host.socket-timeout-millis.

    18.15 Rewriting the Location header

    If Zuul is fronting a web application, you may need to re-write the Location header when the web application redirects through a HTTP status code of 3XX. +Otherwise, the browser redirects to the web application’s URL instead of the Zuul URL. +You can configure a LocationRewriteFilter Zuul filter to re-write the Location header to the Zuul’s URL. +It also adds back the stripped global and route-specific prefixes. +The following example adds a filter by using a Spring Configuration file:

    import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
    +...
    +
    +@Configuration
    +@EnableZuulProxy
    +public class ZuulConfig {
    +    @Bean
    +    public LocationRewriteFilter locationRewriteFilter() {
    +        return new LocationRewriteFilter();
    +    }
    +}
    [Caution]Caution

    Use this filter carefully. The filter acts on the Location header of ALL 3XX response codes, which may not be appropriate in all scenarios, such as when redirecting the user to an external URL.

    18.16 Enabling Cross Origin Requests

    By default Zuul routes all Cross Origin requests (CORS) to the services. If you want instead Zuul to handle these requests it can be done by providing custom WebMvcConfigurer bean:

    @Bean
    +public WebMvcConfigurer corsConfigurer() {
    +    return new WebMvcConfigurer() {
    +        public void addCorsMappings(CorsRegistry registry) {
    +            registry.addMapping("/path-1/**")
    +                    .allowedOrigins("https://allowed-origin.com")
    +                    .allowedMethods("GET", "POST");
    +        }
    +    };
    +}

    In the example above, we allow GET and POST methods from https://allowed-origin.com to send cross-origin requests to the endpoints starting with path-1. +You can apply CORS configuration to a specific path pattern or globally for the whole application, using /** mapping. +You can customize properties: allowedOrigins,allowedMethods,allowedHeaders,exposedHeaders,allowCredentials and maxAge via this configuration.

    18.17 Metrics

    Zuul will provide metrics under the Actuator metrics endpoint for any failures that might occur when routing requests. +These metrics can be viewed by hitting /actuator/metrics. The metrics will have a name that has the format +ZUUL::EXCEPTION:errorCause:statusCode.

    18.18 Zuul Developer Guide

    For a general overview of how Zuul works, see the Zuul Wiki.

    18.18.1 The Zuul Servlet

    Zuul is implemented as a Servlet. For the general cases, Zuul is embedded into the Spring Dispatch mechanism. This lets Spring MVC be in control of the routing. +In this case, Zuul buffers requests. +If there is a need to go through Zuul without buffering requests (for example, for large file uploads), the Servlet is also installed outside of the Spring Dispatcher. +By default, the servlet has an address of /zuul. +This path can be changed with the zuul.servlet-path property.

    18.18.2 Zuul RequestContext

    To pass information between filters, Zuul uses a RequestContext. +Its data is held in a ThreadLocal specific to each request. +Information about where to route requests, errors, and the actual HttpServletRequest and HttpServletResponse are stored there. +The RequestContext extends ConcurrentHashMap, so anything can be stored in the context. FilterConstants contains the keys used by the filters installed by Spring Cloud Netflix (more on these later).

    18.18.3 @EnableZuulProxy vs. @EnableZuulServer

    Spring Cloud Netflix installs a number of filters, depending on which annotation was used to enable Zuul. @EnableZuulProxy is a superset of @EnableZuulServer. In other words, @EnableZuulProxy contains all the filters installed by @EnableZuulServer. The additional filters in the proxy enable routing functionality. If you want a blank Zuul, you should use @EnableZuulServer.

    18.18.4 @EnableZuulServer Filters

    @EnableZuulServer creates a SimpleRouteLocator that loads route definitions from Spring Boot configuration files.

    The following filters are installed (as normal Spring Beans):

    • Pre filters:

      • ServletDetectionFilter: Detects whether the request is through the Spring Dispatcher. Sets a boolean with a key of FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY.
      • FormBodyWrapperFilter: Parses form data and re-encodes it for downstream requests.
      • DebugFilter: If the debug request parameter is set, sets RequestContext.setDebugRouting() and RequestContext.setDebugRequest() to true. +*Route filters:
      • SendForwardFilter: Forwards requests by using the Servlet RequestDispatcher. The forwarding location is stored in the RequestContext attribute, FilterConstants.FORWARD_TO_KEY. This is useful for forwarding to endpoints in the current application.
    • Post filters:

      • SendResponseFilter: Writes responses from proxied requests to the current response.
    • Error filters:

      • SendErrorFilter: Forwards to /error (by default) if RequestContext.getThrowable() is not null. You can change the default forwarding path (/error) by setting the error.path property.

    18.18.5 @EnableZuulProxy Filters

    Creates a DiscoveryClientRouteLocator that loads route definitions from a DiscoveryClient (such as Eureka) as well as from properties. A route is created for each serviceId from the DiscoveryClient. As new services are added, the routes are refreshed.

    In addition to the filters described earlier, the following filters are installed (as normal Spring Beans):

    • Pre filters:

      • PreDecorationFilter: Determines where and how to route, depending on the supplied RouteLocator. It also sets various proxy-related headers for downstream requests.
    • Route filters:

      • RibbonRoutingFilter: Uses Ribbon, Hystrix, and pluggable HTTP clients to send requests. Service IDs are found in the RequestContext attribute, FilterConstants.SERVICE_ID_KEY. This filter can use different HTTP clients:

        • Apache HttpClient: The default client.
        • Squareup OkHttpClient v3: Enabled by having the com.squareup.okhttp3:okhttp library on the classpath and setting ribbon.okhttp.enabled=true.
        • Netflix Ribbon HTTP client: Enabled by setting ribbon.restclient.enabled=true. This client has limitations, including that it does not support the PATCH method, but it also has built-in retry.
      • SimpleHostRoutingFilter: Sends requests to predetermined URLs through an Apache HttpClient. URLs are found in RequestContext.getRouteHost().

    18.18.6 Custom Zuul Filter Examples

    Most of the following "How to Write" examples below are included Sample Zuul Filters project. There are also examples of manipulating the request or response body in that repository.

    This section includes the following examples:

    How to Write a Pre Filter

    Pre filters set up data in the RequestContext for use in filters downstream. +The main use case is to set information required for route filters. +The following example shows a Zuul pre filter:

    public class QueryParamPreFilter extends ZuulFilter {
    +	@Override
    +	public int filterOrder() {
    +		return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
    +	}
    +
    +	@Override
    +	public String filterType() {
    +		return PRE_TYPE;
    +	}
    +
    +	@Override
    +	public boolean shouldFilter() {
    +		RequestContext ctx = RequestContext.getCurrentContext();
    +		return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
    +				&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
    +	}
    +    @Override
    +    public Object run() {
    +        RequestContext ctx = RequestContext.getCurrentContext();
    +		HttpServletRequest request = ctx.getRequest();
    +		if (request.getParameter("sample") != null) {
    +		    // put the serviceId in `RequestContext`
    +    		ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
    +    	}
    +        return null;
    +    }
    +}

    The preceding filter populates SERVICE_ID_KEY from the sample request parameter. +In practice, you should not do that kind of direct mapping. Instead, the service ID should be looked up from the value of sample instead.

    Now that SERVICE_ID_KEY is populated, PreDecorationFilter does not run and RibbonRoutingFilter runs.

    [Tip]Tip

    If you want to route to a full URL, call ctx.setRouteHost(url) instead.

    To modify the path to which routing filters forward, set the REQUEST_URI_KEY.

    How to Write a Route Filter

    Route filters run after pre filters and make requests to other services. +Much of the work here is to translate request and response data to and from the model required by the client. +The following example shows a Zuul route filter:

    public class OkHttpRoutingFilter extends ZuulFilter {
    +	@Autowired
    +	private ProxyRequestHelper helper;
    +
    +	@Override
    +	public String filterType() {
    +		return ROUTE_TYPE;
    +	}
    +
    +	@Override
    +	public int filterOrder() {
    +		return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
    +	}
    +
    +	@Override
    +	public boolean shouldFilter() {
    +		return RequestContext.getCurrentContext().getRouteHost() != null
    +				&& RequestContext.getCurrentContext().sendZuulResponse();
    +	}
    +
    +    @Override
    +    public Object run() {
    +		OkHttpClient httpClient = new OkHttpClient.Builder()
    +				// customize
    +				.build();
    +
    +		RequestContext context = RequestContext.getCurrentContext();
    +		HttpServletRequest request = context.getRequest();
    +
    +		String method = request.getMethod();
    +
    +		String uri = this.helper.buildZuulRequestURI(request);
    +
    +		Headers.Builder headers = new Headers.Builder();
    +		Enumeration<String> headerNames = request.getHeaderNames();
    +		while (headerNames.hasMoreElements()) {
    +			String name = headerNames.nextElement();
    +			Enumeration<String> values = request.getHeaders(name);
    +
    +			while (values.hasMoreElements()) {
    +				String value = values.nextElement();
    +				headers.add(name, value);
    +			}
    +		}
    +
    +		InputStream inputStream = request.getInputStream();
    +
    +		RequestBody requestBody = null;
    +		if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
    +			MediaType mediaType = null;
    +			if (headers.get("Content-Type") != null) {
    +				mediaType = MediaType.parse(headers.get("Content-Type"));
    +			}
    +			requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
    +		}
    +
    +		Request.Builder builder = new Request.Builder()
    +				.headers(headers.build())
    +				.url(uri)
    +				.method(method, requestBody);
    +
    +		Response response = httpClient.newCall(builder.build()).execute();
    +
    +		LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
    +
    +		for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
    +			responseHeaders.put(entry.getKey(), entry.getValue());
    +		}
    +
    +		this.helper.setResponse(response.code(), response.body().byteStream(),
    +				responseHeaders);
    +		context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
    +		return null;
    +    }
    +}

    The preceding filter translates Servlet request information into OkHttp3 request information, executes an HTTP request, and translates OkHttp3 response information to the Servlet response.

    How to Write a Post Filter

    Post filters typically manipulate the response. The following filter adds a random UUID as the X-Sample header:

    public class AddResponseHeaderFilter extends ZuulFilter {
    +	@Override
    +	public String filterType() {
    +		return POST_TYPE;
    +	}
    +
    +	@Override
    +	public int filterOrder() {
    +		return SEND_RESPONSE_FILTER_ORDER - 1;
    +	}
    +
    +	@Override
    +	public boolean shouldFilter() {
    +		return true;
    +	}
    +
    +	@Override
    +	public Object run() {
    +		RequestContext context = RequestContext.getCurrentContext();
    +    	HttpServletResponse servletResponse = context.getResponse();
    +		servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
    +		return null;
    +	}
    +}
    [Note]Note

    Other manipulations, such as transforming the response body, are much more complex and computationally intensive.

    18.18.7 How Zuul Errors Work

    If an exception is thrown during any portion of the Zuul filter lifecycle, the error filters are executed. +The SendErrorFilter is only run if RequestContext.getThrowable() is not null. +It then sets specific javax.servlet.error.* attributes in the request and forwards the request to the Spring Boot error page.

    18.18.8 Zuul Eager Application Context Loading

    Zuul internally uses Ribbon for calling the remote URLs. +By default, Ribbon clients are lazily loaded by Spring Cloud on first call. +This behavior can be changed for Zuul by using the following configuration, which results eager loading of the child Ribbon related Application contexts at application startup time. +The following example shows how to enable eager loading:

    application.yml.  +

    zuul:
    +  ribbon:
    +    eager-load:
    +      enabled: true

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__running_examples.html b/Greenwich.SR5/multi/multi__running_examples.html new file mode 100644 index 00000000..6fca3fad --- /dev/null +++ b/Greenwich.SR5/multi/multi__running_examples.html @@ -0,0 +1,7 @@ + + + 65. Running examples

    65. Running examples

    You can see the running examples deployed in the Pivotal Web Services. +Check them out at the following links:

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__sample_13.html b/Greenwich.SR5/multi/multi__sample_13.html new file mode 100644 index 00000000..a30d0ea7 --- /dev/null +++ b/Greenwich.SR5/multi/multi__sample_13.html @@ -0,0 +1,3 @@ + + + 170. Sample

    170. Sample

    A Kotlin sample application is provided to demonstrate a working Maven setup and various Spring Cloud GCP integrations from within Kotlin.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__samples.html b/Greenwich.SR5/multi/multi__samples.html new file mode 100644 index 00000000..183b9f58 --- /dev/null +++ b/Greenwich.SR5/multi/multi__samples.html @@ -0,0 +1,3 @@ + + + 38. Samples

    38. Samples

    For Spring Cloud Stream samples, see the spring-cloud-stream-samples repository on GitHub.

    38.1 Deploying Stream Applications on CloudFoundry

    On CloudFoundry, services are usually exposed through a special environment variable called VCAP_SERVICES.

    When configuring your binder connections, you can use the values from an environment variable as explained on the dataflow Cloud Foundry Server docs.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__sampling.html b/Greenwich.SR5/multi/multi__sampling.html new file mode 100644 index 00000000..d9a345c9 --- /dev/null +++ b/Greenwich.SR5/multi/multi__sampling.html @@ -0,0 +1,53 @@ + + + 53. Sampling

    53. Sampling

    Sampling may be employed to reduce the data collected and reported out of process. +When a span is not sampled, it adds no overhead (a noop).

    Sampling is an up-front decision, meaning that the decision to report data is made at the first operation in a trace and that decision is propagated downstream.

    By default, a global sampler applies a single rate to all traced operations. +Tracer.Builder.sampler controls this setting, and it defaults to tracing every request.

    53.1 Declarative sampling

    Some applications need to sample based on the type or annotations of a java method.

    Most users use a framework interceptor to automate this sort of policy. +The following example shows how that might work internally:

    @Autowired Tracer tracer;
    +
    +// derives a sample rate from an annotation on a java method
    +DeclarativeSampler<Traced> sampler = DeclarativeSampler.create(Traced::sampleRate);
    +
    +@Around("@annotation(traced)")
    +public Object traceThing(ProceedingJoinPoint pjp, Traced traced) throws Throwable {
    +  // When there is no trace in progress, this decides using an annotation
    +  Sampler decideUsingAnnotation = declarativeSampler.toSampler(traced);
    +  Tracer tracer = tracer.withSampler(decideUsingAnnotation);
    +
    +  // This code looks the same as if there was no declarative override
    +  ScopedSpan span = tracer.startScopedSpan(spanName(pjp));
    +  try {
    +    return pjp.proceed();
    +  } catch (RuntimeException | Error e) {
    +    span.error(e);
    +    throw e;
    +  } finally {
    +    span.finish();
    +  }
    +}

    53.2 Custom sampling

    Depending on what the operation is, you may want to apply different policies. +For example, you might not want to trace requests to static resources such as images, or you might want to trace all requests to a new api.

    Most users use a framework interceptor to automate this sort of policy. +The following example shows how that might work internally:

    @Autowired Tracer tracer;
    +@Autowired Sampler fallback;
    +
    +Span nextSpan(final Request input) {
    +  Sampler requestBased = Sampler() {
    +    @Override public boolean isSampled(long traceId) {
    +      if (input.url().startsWith("/experimental")) {
    +        return true;
    +      } else if (input.url().startsWith("/static")) {
    +        return false;
    +      }
    +      return fallback.isSampled(traceId);
    +    }
    +  };
    +  return tracer.withSampler(requestBased).nextSpan();
    +}

    53.3 Sampling in Spring Cloud Sleuth

    By default Spring Cloud Sleuth sets all spans to non-exportable. +That means that traces appear in logs but not in any remote store. +For testing the default is often enough, and it probably is all you need if you use only the logs (for example, with an ELK aggregator). +If you export span data to Zipkin, there is also an Sampler.ALWAYS_SAMPLE setting that exports everything and a ProbabilityBasedSampler setting that samples a fixed fraction of spans.

    [Note]Note

    The ProbabilityBasedSampler is the default if you use spring-cloud-sleuth-zipkin. +You can configure the exports by setting spring.sleuth.sampler.probability. +The passed value needs to be a double from 0.0 to 1.0.

    A sampler can be installed by creating a bean definition, as shown in the following example:

    @Bean
    +public Sampler defaultSampler() {
    +	return Sampler.ALWAYS_SAMPLE;
    +}
    [Tip]Tip

    You can set the HTTP header X-B3-Flags to 1, or, when doing messaging, you can set the spanFlags header to 1. +Doing so forces the current span to be exportable regardless of the sampling decision.

    In order to use the rate-limited sampler set the spring.sleuth.sampler.rate property to choose an amount of traces to accept on a per-second interval. The minimum number is 0 and the max is 2,147,483,647 (max int).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__security_configurations_inside_kubernetes.html b/Greenwich.SR5/multi/multi__security_configurations_inside_kubernetes.html new file mode 100644 index 00000000..a2ae26c6 --- /dev/null +++ b/Greenwich.SR5/multi/multi__security_configurations_inside_kubernetes.html @@ -0,0 +1,9 @@ + + + 145. Security Configurations Inside Kubernetes

    145. Security Configurations Inside Kubernetes

    145.1 Namespace

    Most of the components provided in this project need to know the namespace. For Kubernetes (1.3+), the namespace is made available to the pod as part of the service account secret and is automatically detected by the client. +For earlier versions, it needs to be specified as an environment variable to the pod. A quick way to do this is as follows:

          env:
    +      - name: "KUBERNETES_NAMESPACE"
    +        valueFrom:
    +          fieldRef:
    +            fieldPath: "metadata.namespace"

    145.2 Service Account

    For distributions of Kubernetes that support more fine-grained role-based access within the cluster, you need to make sure a pod that runs with spring-cloud-kubernetes has access to the Kubernetes API. +For any service accounts you assign to a deployment or pod, you need to make sure they have the correct roles. For example, you can add cluster-reader permissions to your default service account, depending on the project you’re in.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__sending_spans_to_zipkin.html b/Greenwich.SR5/multi/multi__sending_spans_to_zipkin.html new file mode 100644 index 00000000..17128661 --- /dev/null +++ b/Greenwich.SR5/multi/multi__sending_spans_to_zipkin.html @@ -0,0 +1,28 @@ + + + 62. Sending Spans to Zipkin

    62. Sending Spans to Zipkin

    By default, if you add spring-cloud-starter-zipkin as a dependency to your project, when the span is closed, it is sent to Zipkin over HTTP. +The communication is asynchronous. +You can configure the URL by setting the spring.zipkin.baseUrl property, as follows:

    spring.zipkin.baseUrl: https://192.168.99.100:9411/

    If you want to find Zipkin through service discovery, you can pass the Zipkin’s service ID inside the URL, as shown in the following example for zipkinserver service ID:

    spring.zipkin.baseUrl: http://zipkinserver/

    To disable this feature just set spring.zipkin.discoveryClientEnabled to `false.

    When the Discovery Client feature is enabled, Sleuth uses +LoadBalancerClient to find the URL of the Zipkin Server. It means +that you can set up the load balancing configuration e.g. via Ribbon.

    zipkinserver:
    +  ribbon:
    +    ListOfServers: host1,host2

    If you have web, rabbit, or kafka together on the classpath, you might need to pick the means by which you would like to send spans to zipkin. +To do so, set web, rabbit, or kafka to the spring.zipkin.sender.type property. +The following example shows setting the sender type for web:

    spring.zipkin.sender.type: web

    To customize the RestTemplate that sends spans to Zipkin via HTTP, you can register +the ZipkinRestTemplateCustomizer bean.

    @Configuration
    +class MyConfig {
    +	@Bean ZipkinRestTemplateCustomizer myCustomizer() {
    +		return new ZipkinRestTemplateCustomizer() {
    +			@Override
    +			void customize(RestTemplate restTemplate) {
    +				// customize the RestTemplate
    +			}
    +		};
    +	}
    +}

    If, however, you would like to control the full process of creating the RestTemplate +object, you will have to create a bean of zipkin2.reporter.Sender type.

    	@Bean Sender myRestTemplateSender(ZipkinProperties zipkin,
    +			ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer) {
    +		RestTemplate restTemplate = mySuperCustomRestTemplate();
    +		zipkinRestTemplateCustomizer.customize(restTemplate);
    +		return myCustomSender(zipkin, restTemplate);
    +	}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__serverless_platform_adapters.html b/Greenwich.SR5/multi/multi__serverless_platform_adapters.html new file mode 100644 index 00000000..6575fe82 --- /dev/null +++ b/Greenwich.SR5/multi/multi__serverless_platform_adapters.html @@ -0,0 +1,49 @@ + + + 135. Serverless Platform Adapters

    135. Serverless Platform Adapters

    As well as being able to run as a standalone process, a Spring Cloud +Function application can be adapted to run one of the existing +serverless platforms. In the project there are adapters for +AWS +Lambda, +Azure, +and +Apache +OpenWhisk. The Oracle Fn platform +has its own Spring Cloud Function adapter. And +Riff supports Java functions and its +Java Function +Invoker acts natively is an adapter for Spring Cloud Function jars.

    135.1 AWS Lambda

    The AWS adapter takes a Spring Cloud Function app and converts it to a form that can run in AWS Lambda.

    135.1.1 Introduction

    The adapter has a couple of generic request handlers that you can use. The most generic is SpringBootStreamHandler, which uses a Jackson ObjectMapper provided by Spring Boot to serialize and deserialize the objects in the function. There is also a SpringBootRequestHandler which you can extend, and provide the input and output types as type parameters (enabling AWS to inspect the class and do the JSON conversions itself).

    If your app has more than one @Bean of type Function etc. then you can choose the one to use by configuring function.name (e.g. as FUNCTION_NAME environment variable in AWS). The functions are extracted from the Spring Cloud FunctionCatalog (searching first for Function then Consumer and finally Supplier).

    135.1.2 Notes on JAR Layout

    You don’t need the Spring Cloud Function Web or Stream adapter at runtime in Lambda, so you might need to exclude those before you create the JAR you send to AWS. A Lambda application has to be shaded, but a Spring Boot standalone application does not, so you can run the same app using 2 separate jars (as per the sample). The sample app creates 2 jar files, one with an aws classifier for deploying in Lambda, and one executable (thin) jar that includes spring-cloud-function-web at runtime. Spring Cloud Function will try and locate a "main class" for you from the JAR file manifest, using the Start-Class attribute (which will be added for you by the Spring Boot tooling if you use the starter parent). If there is no Start-Class in your manifest you can use an environment variable MAIN_CLASS when you deploy the function to AWS.

    135.1.3 Upload

    Build the sample under spring-cloud-function-samples/function-sample-aws and upload the -aws jar file to Lambda. The handler can be example.Handler or org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler (FQN of the class, not a method reference, although Lambda does accept method references).

    ./mvnw -U clean package

    Using the AWS command line tools it looks like this:

    aws lambda create-function --function-name Uppercase --role arn:aws:iam::[USERID]:role/service-role/[ROLE] --zip-file fileb://function-sample-aws/target/function-sample-aws-2.0.0.BUILD-SNAPSHOT-aws.jar --handler org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler --description "Spring Cloud Function Adapter Example" --runtime java8 --region us-east-1 --timeout 30 --memory-size 1024 --publish

    The input type for the function in the AWS sample is a Foo with a single property called "value". So you would need this to test it:

    {
    +  "value": "test"
    +}
    [Note]Note

    The AWS sample app is written in the "functional" style (as an ApplicationContextInitializer). This is much faster on startup in Lambda than the traditional @Bean style, so if you don’t need @Beans (or @EnableAutoConfiguration) it’s a good choice. Warm starts are not affected.

    135.1.4 Platfom Specific Features

    HTTP and API Gateway

    AWS has some platform-specific data types, including batching of messages, which is much more efficient than processing each one individually. To make use of these types you can write a function that depends on those types. Or you can rely on Spring to extract the data from the AWS types and convert it to a Spring Message. To do this you tell AWS that the function is of a specific generic handler type (depending on the AWS service) and provide a bean of type Function<Message<S>,Message<T>>, where S and T are your business data types. If there is more than one bean of type Function you may also need to configure the Spring Boot property function.name to be the name of the target bean (e.g. use FUNCTION_NAME as an environment variable).

    The supported AWS services and generic handler types are listed below:

    ServiceAWS TypesGeneric Handler 

    API Gateway

    APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent

    org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler

     

    Kinesis

    KinesisEvent

    org.springframework.cloud.function.adapter.aws.SpringBootKinesisEventHandler

     

    For example, to deploy behind an API Gateway, use --handler org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler in your AWS command line (in via the UI) and define a @Bean of type Function<Message<Foo>,Message<Bar>> where Foo and Bar are POJO types (the data will be marshalled and unmarshalled by AWS using Jackson).

    135.2 Azure Functions

    The Azure adapter bootstraps a Spring Cloud Function context and channels function calls from the Azure framework into the user functions, using Spring Boot configuration where necessary. Azure Functions has quite a unique, but invasive programming model, involving annotations in user code that are specific to the platform. The easiest way to use it with Spring Cloud is to extend a base class and write a method in it with the @FunctionName annotation which delegates to a base class method.

    This project provides an adapter layer for a Spring Cloud Function application onto Azure. +You can write an app with a single @Bean of type Function and it will be deployable in Azure if you get the JAR file laid out right.

    There is an AzureSpringBootRequestHandler which you must extend, and provide the input and output types as annotated method parameters (enabling Azure to inspect the class and create JSON bindings). The base class has two useful methods (handleRequest and handleOutput) to which you can delegate the actual function call, so mostly the function will only ever have one line.

    Example:

    public class FooHandler extends AzureSpringBootRequestHandler<Foo, Bar> {
    +	@FunctionName("uppercase")
    +	public Bar execute(
    +			@HttpTrigger(name = "req", methods = { HttpMethod.GET,
    +					HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS)
    +                    Foo foo,
    +			ExecutionContext context) {
    +		return handleRequest(foo, context);
    +	}
    +}

    This Azure handler will delegate to a Function<Foo,Bar> bean (or a Function<Publisher<Foo>,Publisher<Bar>>). Some Azure triggers (e.g. @CosmosDBTrigger) result in a input type of List and in that case you can bind to List in the Azure handler, or String (the raw JSON). The List input delegates to a Function with input type Map<String,Object>, or Publisher or List of the same type. The output of the Function can be a List (one-for-one) or a single value (aggregation), and the output binding in the Azure declaration should match.

    If your app has more than one @Bean of type Function etc. then you can choose the one to use by configuring function.name. Or if you make the @FunctionName in the Azure handler method match the function name it should work that way (also for function apps with multiple functions). The functions are extracted from the Spring Cloud FunctionCatalog so the default function names are the same as the bean names.

    135.2.1 Notes on JAR Layout

    You don’t need the Spring Cloud Function Web at runtime in Azure, so you can exclude this before you create the JAR you deploy to Azure, but it won’t be used if you include it so it doesn’t hurt to leave it in. A function application on Azure is an archive generated by the Maven plugin. The function lives in the JAR file generated by this project. The sample creates it as an executable jar, using the thin layout, so that Azure can find the handler classes. If you prefer you can just use a regular flat JAR file. The dependencies should not be included.

    135.2.2 Build

    ./mvnw -U clean package

    135.2.3 Running the sample

    You can run the sample locally, just like the other Spring Cloud Function samples:

    and curl -H "Content-Type: text/plain" localhost:8080/function -d '{"value": "hello foobar"}'.

    You will need the az CLI app (see https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-java-maven for more detail). To deploy the function on Azure runtime:

    $ az login
    +$ mvn azure-functions:deploy

    On another terminal try this: curl https://<azure-function-url-from-the-log>/api/uppercase -d '{"value": "hello foobar!"}'. Please ensure that you use the right URL for the function above. Alternatively you can test the function in the Azure Dashboard UI (click on the function name, go to the right hand side and click "Test" and to the bottom right, "Run").

    The input type for the function in the Azure sample is a Foo with a single property called "value". So you need this to test it with something like below:

    {
    +  "value": "foobar"
    +}
    [Note]Note

    The Azure sample app is written in the "non-functional" style (using @Bean). The functional style (with just Function or ApplicationContextInitializer) is much faster on startup in Azure than the traditional @Bean style, so if you don’t need @Beans (or @EnableAutoConfiguration) it’s a good choice. Warm starts are not affected.

    135.3 Apache Openwhisk

    The OpenWhisk adapter is in the form of an executable jar that can be used in a a docker image to be deployed to Openwhisk. The platform works in request-response mode, listening on port 8080 on a specific endpoint, so the adapter is a simple Spring MVC application.

    135.3.1 Quick Start

    Implement a POF (be sure to use the functions package):

    package functions;
    +
    +import java.util.function.Function;
    +
    +public class Uppercase implements Function<String, String> {
    +
    +	public String apply(String input) {
    +		return input.toUpperCase();
    +	}
    +}

    Install it into your local Maven repository:

    ./mvnw clean install

    Create a function.properties file that provides its Maven coordinates. For example:

    dependencies.function: com.example:pof:0.0.1-SNAPSHOT

    Copy the openwhisk runner JAR to the working directory (same directory as the properties file):

    cp spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk/target/spring-cloud-function-adapter-openwhisk-2.0.0.BUILD-SNAPSHOT.jar runner.jar

    Generate a m2 repo from the --thin.dryrun of the runner JAR with the above properties file:

    java -jar -Dthin.root=m2 runner.jar --thin.name=function --thin.dryrun

    Use the following Dockerfile:

    FROM openjdk:8-jdk-alpine
    +VOLUME /tmp
    +COPY m2 /m2
    +ADD runner.jar .
    +ADD function.properties .
    +ENV JAVA_OPTS=""
    +ENTRYPOINT [ "java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "runner.jar", "--thin.root=/m2", "--thin.name=function", "--function.name=uppercase"]
    +EXPOSE 8080
    [Note]Note

    you could use a Spring Cloud Function app, instead of just a jar with a POF in it, in which case you would have to change the way the app runs in the container so that it picks up the main class as a source file. For example, you could change the ENTRYPOINT above and add --spring.main.sources=com.example.SampleApplication.

    Build the Docker image:

    docker build -t [username/appname] .

    Push the Docker image:

    docker push [username/appname]

    Use the OpenWhisk CLI (e.g. after vagrant ssh) to create the action:

    wsk action create example --docker [username/appname]

    Invoke the action:

    wsk action invoke example --result --param payload foo
    +{
    +    "result": "FOO"
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__service_discovery_eureka_clients.html b/Greenwich.SR5/multi/multi__service_discovery_eureka_clients.html new file mode 100644 index 00000000..bb32112e --- /dev/null +++ b/Greenwich.SR5/multi/multi__service_discovery_eureka_clients.html @@ -0,0 +1,144 @@ + + + 11. Service Discovery: Eureka Clients

    11. Service Discovery: Eureka Clients

    Service Discovery is one of the key tenets of a microservice-based architecture. +Trying to hand-configure each client or some form of convention can be difficult to do and can be brittle. +Eureka is the Netflix Service Discovery Server and Client. +The server can be configured and deployed to be highly available, with each server replicating state about the registered services to the others.

    11.1 How to Include Eureka Client

    To include the Eureka Client in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-eureka-client. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    11.2 Registering with Eureka

    When a client registers with Eureka, it provides meta-data about itself — such as host, port, health indicator URL, home page, and other details. +Eureka receives heartbeat messages from each instance belonging to a service. +If the heartbeat fails over a configurable timetable, the instance is normally removed from the registry.

    The following example shows a minimal Eureka client application:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello world";
    +    }
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}

    Note that the preceding example shows a normal Spring Boot application. +By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server. Configuration is required to locate the Eureka server, as shown in the following example:

    application.yml.  +

    eureka:
    +  client:
    +    serviceUrl:
    +      defaultZone: http://localhost:8761/eureka/

    +

    In the preceding example, defaultZone is a magic string fallback value that provides the service URL for any client that does not express a preference (in other words, it is a useful default).

    [Warning]Warning

    The defaultZone property is case sensitive and requires camel case because the serviceUrl property is a Map<String, String>. Therefore, the defaultZone property does not follow the normal Spring Boot snake-case convention of default-zone.

    The default application name (that is, the service ID), virtual host, and non-secure port (taken from the Environment) are ${spring.application.name}, ${spring.application.name} and ${server.port}, respectively.

    Having spring-cloud-starter-netflix-eureka-client on the classpath makes the app into both a Eureka instance (that is, it registers itself) and a client (it can query the registry to locate other services). +The instance behaviour is driven by eureka.instance.* configuration keys, but the defaults are fine if you ensure that your application has a value for spring.application.name (this is the default for the Eureka service ID or VIP).

    See EurekaInstanceConfigBean and EurekaClientConfigBean for more details on the configurable options.

    To disable the Eureka Discovery Client, you can set eureka.client.enabled to false. Eureka Discovery Client will also be disabled when spring.cloud.discovery.enabled is set to false.

    11.3 Authenticating with the Eureka Server

    HTTP basic authentication is automatically added to your eureka client if one of the eureka.client.serviceUrl.defaultZone URLs has credentials embedded in it (curl style, as follows: http://user:password@localhost:8761/eureka). +For more complex needs, you can create a @Bean of type DiscoveryClientOptionalArgs and inject ClientFilter instances into it, all of which is applied to the calls from the client to the server.

    [Note]Note

    Because of a limitation in Eureka, it is not possible to support per-server basic auth credentials, so only the first set that are found is used.

    11.4 Status Page and Health Indicator

    The status page and health indicators for a Eureka instance default to /info and /health respectively, which are the default locations of useful endpoints in a Spring Boot Actuator application. +You need to change these, even for an Actuator application if you use a non-default context path or servlet path (such as server.servletPath=/custom). The following example shows the default values for the two settings:

    application.yml.  +

    eureka:
    +  instance:
    +    statusPageUrlPath: ${server.servletPath}/info
    +    healthCheckUrlPath: ${server.servletPath}/health

    +

    These links show up in the metadata that is consumed by clients and are used in some scenarios to decide whether to send requests to your application, so it is helpful if they are accurate.

    [Note]Note

    In Dalston it was also required to set the status and health check URLs when changing +that management context path. This requirement was removed beginning in Edgware.

    11.5 Registering a Secure Application

    If your app wants to be contacted over HTTPS, you can set two flags in the EurekaInstanceConfig:

    • eureka.instance.[nonSecurePortEnabled]=[false]
    • eureka.instance.[securePortEnabled]=[true]

    Doing so makes Eureka publish instance information that shows an explicit preference for secure communication. +The Spring Cloud DiscoveryClient always returns a URI starting with https for a service configured this way. +Similarly, when a service is configured this way, the Eureka (native) instance information has a secure health check URL.

    Because of the way Eureka works internally, it still publishes a non-secure URL for the status and home pages unless you also override those explicitly. +You can use placeholders to configure the eureka instance URLs, as shown in the following example:

    application.yml.  +

    eureka:
    +  instance:
    +    statusPageUrl: https://${eureka.hostname}/info
    +    healthCheckUrl: https://${eureka.hostname}/health
    +    homePageUrl: https://${eureka.hostname}/

    +

    (Note that ${eureka.hostname} is a native placeholder only available +in later versions of Eureka. You could achieve the same thing with +Spring placeholders as well — for example, by using ${eureka.instance.hostName}.)

    [Note]Note

    If your application runs behind a proxy, and the SSL termination is in the proxy (for example, if you run in Cloud Foundry or other platforms as a service), then you need to ensure that the proxy forwarded headers are intercepted and handled by the application. +If the Tomcat container embedded in a Spring Boot application has explicit configuration for the 'X-Forwarded-\*` headers, this happens automatically. +The links rendered by your app to itself being wrong (the wrong host, port, or protocol) is a sign that you got this configuration wrong.

    11.6 Eureka’s Health Checks

    By default, Eureka uses the client heartbeat to determine if a client is up. +Unless specified otherwise, the Discovery Client does not propagate the current health check status of the application, per the Spring Boot Actuator. +Consequently, after successful registration, Eureka always announces that the application is in 'UP' state. This behavior can be altered by enabling Eureka health checks, which results in propagating application status to Eureka. +As a consequence, every other application does not send traffic to applications in states other then 'UP'. +The following example shows how to enable health checks for the client:

    application.yml.  +

    eureka:
    +  client:
    +    healthcheck:
    +      enabled: true

    +

    [Warning]Warning

    eureka.client.healthcheck.enabled=true should only be set in application.yml. Setting the value in bootstrap.yml causes undesirable side effects, such as registering in Eureka with an UNKNOWN status.

    If you require more control over the health checks, consider implementing your own com.netflix.appinfo.HealthCheckHandler.

    11.7 Eureka Metadata for Instances and Clients

    It is worth spending a bit of time understanding how the Eureka metadata works, so you can use it in a way that makes sense in your platform. +There is standard metadata for information such as hostname, IP address, port numbers, the status page, and health check. +These are published in the service registry and used by clients to contact the services in a straightforward way. +Additional metadata can be added to the instance registration in the eureka.instance.metadataMap, and this metadata is accessible in the remote clients. +In general, additional metadata does not change the behavior of the client, unless the client is made aware of the meaning of the metadata. +There are a couple of special cases, described later in this document, where Spring Cloud already assigns meaning to the metadata map.

    11.7.1 Using Eureka on Cloud Foundry

    Cloud Foundry has a global router so that all instances of the same app have the same hostname (other PaaS solutions with a similar architecture have the same arrangement). +This is not necessarily a barrier to using Eureka. +However, if you use the router (recommended or even mandatory, depending on the way your platform was set up), you need to explicitly set the hostname and port numbers (secure or non-secure) so that they use the router. +You might also want to use instance metadata so that you can distinguish between the instances on the client (for example, in a custom load balancer). +By default, the eureka.instance.instanceId is vcap.application.instance_id, as shown in the following example:

    application.yml.  +

    eureka:
    +  instance:
    +    hostname: ${vcap.application.uris[0]}
    +    nonSecurePort: 80

    +

    Depending on the way the security rules are set up in your Cloud Foundry instance, you might be able to register and use the IP address of the host VM for direct service-to-service calls. +This feature is not yet available on Pivotal Web Services (PWS).

    11.7.2 Using Eureka on AWS

    If the application is planned to be deployed to an AWS cloud, the Eureka instance must be configured to be AWS-aware. You can do so by customizing the EurekaInstanceConfigBean as follows:

    @Bean
    +@Profile("!default")
    +public EurekaInstanceConfigBean eurekaInstanceConfig(InetUtils inetUtils) {
    +  EurekaInstanceConfigBean b = new EurekaInstanceConfigBean(inetUtils);
    +  AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka");
    +  b.setDataCenterInfo(info);
    +  return b;
    +}

    11.7.3 Changing the Eureka Instance ID

    A vanilla Netflix Eureka instance is registered with an ID that is equal to its host name (that is, there is only one service per host). +Spring Cloud Eureka provides a sensible default, which is defined as follows:

    ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}}

    An example is myhost:myappname:8080.

    By using Spring Cloud, you can override this value by providing a unique identifier in eureka.instance.instanceId, as shown in the following example:

    application.yml.  +

    eureka:
    +  instance:
    +    instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}

    +

    With the metadata shown in the preceding example and multiple service instances deployed on localhost, the random value is inserted there to make the instance unique. +In Cloud Foundry, the vcap.application.instance_id is populated automatically in a Spring Boot application, so the random value is not needed.

    11.8 Using the EurekaClient

    Once you have an application that is a discovery client, you can use it to discover service instances from the Eureka Server. +One way to do so is to use the native com.netflix.discovery.EurekaClient (as opposed to the Spring Cloud DiscoveryClient), as shown in the following example:

    @Autowired
    +private EurekaClient discoveryClient;
    +
    +public String serviceUrl() {
    +    InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false);
    +    return instance.getHomePageUrl();
    +}
    [Tip]Tip

    Do not use the EurekaClient in a @PostConstruct method or in a @Scheduled method (or anywhere where the ApplicationContext might not be started yet). +It is initialized in a SmartLifecycle (with phase=0), so the earliest you can rely on it being available is in another SmartLifecycle with a higher phase.

    11.8.1 EurekaClient without Jersey

    By default, EurekaClient uses Jersey for HTTP communication. +If you wish to avoid dependencies from Jersey, you can exclude it from your dependencies. +Spring Cloud auto-configures a transport client based on Spring RestTemplate. +The following example shows Jersey being excluded:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>com.sun.jersey</groupId>
    +            <artifactId>jersey-client</artifactId>
    +        </exclusion>
    +        <exclusion>
    +            <groupId>com.sun.jersey</groupId>
    +            <artifactId>jersey-core</artifactId>
    +        </exclusion>
    +        <exclusion>
    +            <groupId>com.sun.jersey.contribs</groupId>
    +            <artifactId>jersey-apache-client4</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>

    11.9 Alternatives to the Native Netflix EurekaClient

    You need not use the raw Netflix EurekaClient. +Also, it is usually more convenient to use it behind a wrapper of some sort. +Spring Cloud has support for Feign (a REST client builder) and Spring RestTemplate through the logical Eureka service identifiers (VIPs) instead of physical URLs. +To configure Ribbon with a fixed list of physical servers, you can set <client>.ribbon.listOfServers to a comma-separated list of physical addresses (or hostnames), where <client> is the ID of the client.

    You can also use the org.springframework.cloud.client.discovery.DiscoveryClient, which provides a simple API (not specific to Netflix) for discovery clients, as shown in the following example:

    @Autowired
    +private DiscoveryClient discoveryClient;
    +
    +public String serviceUrl() {
    +    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    +    if (list != null && list.size() > 0 ) {
    +        return list.get(0).getUri();
    +    }
    +    return null;
    +}

    11.10 Why Is It so Slow to Register a Service?

    Being an instance also involves a periodic heartbeat to the registry +(through the client’s serviceUrl) with a default duration of 30 seconds. +A service is not available for discovery by clients until the instance, the server, and the client all have the same metadata in their local +cache (so it could take 3 heartbeats). +You can change the period by setting eureka.instance.leaseRenewalIntervalInSeconds. +Setting it to a value of less than 30 speeds up the process of getting clients connected to other services. +In production, it is probably better to stick with the default, because of internal computations in the server that make assumptions about the lease renewal period.

    11.11 Zones

    If you have deployed Eureka clients to multiple zones, you may prefer that those clients use services within the same zone before trying services in another zone. +To set that up, you need to configure your Eureka clients correctly.

    First, you need to make sure you have Eureka servers deployed to each zone and that +they are peers of each other. +See the section on zones and regions +for more information.

    Next, you need to tell Eureka which zone your service is in. +You can do so by using the metadataMap property. +For example, if service 1 is deployed to both zone 1 and zone 2, you need to set the following Eureka properties in service 1:

    Service 1 in Zone 1

    eureka.instance.metadataMap.zone = zone1
    +eureka.client.preferSameZoneEureka = true

    Service 1 in Zone 2

    eureka.instance.metadataMap.zone = zone2
    +eureka.client.preferSameZoneEureka = true

    11.12 Refreshing Eureka Clients

    By default, the EurekaClient bean is refreshable, meaning the Eureka client properties can be changed and refreshed. +When a refresh occurs clients will be unregistered from the Eureka server and there might be a brief moment of time +where all instance of a given service are not available. One way to eliminate this from happening is to disable +the ability to refresh Eureka clients. To do this set eureka.client.refresh.enable=false.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__service_id_must_be_unique.html b/Greenwich.SR5/multi/multi__service_id_must_be_unique.html new file mode 100644 index 00000000..1df71570 --- /dev/null +++ b/Greenwich.SR5/multi/multi__service_id_must_be_unique.html @@ -0,0 +1,9 @@ + + + 46. Service ID Must Be Unique

    46. Service ID Must Be Unique

    The bus tries twice to eliminate processing an event — once from the original +ApplicationEvent and once from the queue. To do so, it checks the sending service ID +against the current service ID. If multiple instances of a service have the same ID, +events are not processed. When running on a local machine, each service is on a different +port, and that port is part of the ID. Cloud Foundry supplies an index to differentiate. +To ensure that the ID is unique outside Cloud Foundry, set spring.application.index to +something unique for each instance of a service.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__service_registry_configuration.html b/Greenwich.SR5/multi/multi__service_registry_configuration.html new file mode 100644 index 00000000..c53e7539 --- /dev/null +++ b/Greenwich.SR5/multi/multi__service_registry_configuration.html @@ -0,0 +1,17 @@ + + + 106. Service Registry Configuration

    106. Service Registry Configuration

    You can use a DiscoveryClient (such as from Spring Cloud Consul) to locate +a Vault server by setting spring.cloud.vault.discovery.enabled=true (default false). +The net result of that is that your apps need a bootstrap.yml (or an environment variable) +with the appropriate discovery configuration. +The benefit is that the Vault can change its co-ordinates, as long as the discovery service +is a fixed point. The default service id is vault but you can change that on the client with +spring.cloud.vault.discovery.serviceId.

    The discovery client implementations all support some kind of metadata map +(e.g. for Eureka we have eureka.instance.metadataMap). Some additional properties of the service +may need to be configured in its service registration metadata so that clients can connect +correctly. Service registries that do not provide details about transport layer security +need to provide a scheme metadata entry to be set either to https or http. +If no scheme is configured and the service is not exposed as secure service, then +configuration defaults to spring.cloud.vault.scheme which is https when it’s not set.

    spring.cloud.vault.discovery:
    +    enabled: true
    +    service-id: my-vault-service
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__service_registry_implementation.html b/Greenwich.SR5/multi/multi__service_registry_implementation.html new file mode 100644 index 00000000..b08c2f10 --- /dev/null +++ b/Greenwich.SR5/multi/multi__service_registry_implementation.html @@ -0,0 +1,5 @@ + + + 146. Service Registry Implementation

    146. Service Registry Implementation

    In Kubernetes service registration is controlled by the platform, the application itself does not control +registration as it may do in other platforms. For this reason using spring.cloud.service-registry.auto-registration.enabled +or setting @EnableDiscoveryClient(autoRegister=false) will have no effect in Spring Cloud Kubernetes.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__serving_alternative_formats.html b/Greenwich.SR5/multi/multi__serving_alternative_formats.html new file mode 100644 index 00000000..df98c235 --- /dev/null +++ b/Greenwich.SR5/multi/multi__serving_alternative_formats.html @@ -0,0 +1,9 @@ + + + 6. Serving Alternative Formats

    6. Serving Alternative Formats

    The default JSON format from the environment endpoints is perfect for consumption by Spring applications, because it maps directly onto the Environment abstraction. +If you prefer, you can consume the same data as YAML or Java properties by adding a suffix (".yml", ".yaml" or ".properties") to the resource path. +This can be useful for consumption by applications that do not care about the structure of the JSON endpoints or the extra metadata they provide (for example, an application that is not using Spring might benefit from the simplicity of this approach).

    The YAML and properties representations have an additional flag (provided as a boolean query parameter called resolvePlaceholders) to signal that placeholders in the source documents (in the standard Spring ${…​} form) should be resolved in the output before rendering, where possible. +This is a useful feature for consumers that do not know about the Spring placeholder conventions.

    [Note]Note

    There are limitations in using the YAML or properties formats, mainly in relation to the loss of metadata. +For example, the JSON is structured as an ordered list of property sources, with names that correlate with the source. +The YAML and properties forms are coalesced into a single map, even if the origin of the values has multiple sources, and the names of the original source files are lost. +Also, the YAML representation is not necessarily a faithful representation of the YAML source in a backing repository either. It is constructed from a list of flat property sources, and assumptions have to be made about the form of the keys.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__serving_plain_text.html b/Greenwich.SR5/multi/multi__serving_plain_text.html new file mode 100644 index 00000000..917d9c19 --- /dev/null +++ b/Greenwich.SR5/multi/multi__serving_plain_text.html @@ -0,0 +1,29 @@ + + + 7. Serving Plain Text

    7. Serving Plain Text

    Instead of using the Environment abstraction (or one of the alternative representations of it in YAML or properties format), your applications might need generic plain-text configuration files that are tailored to their environment. +The Config Server provides these through an additional endpoint at /{application}/{profile}/{label}/{path}, where application, profile, and label have the same meaning as the regular environment endpoint, but path is a path to a file name (such as log.xml). +The source files for this endpoint are located in the same way as for the environment endpoints. +The same search path is used for properties and YAML files. +However, instead of aggregating all matching resources, only the first one to match is returned.

    After a resource is located, placeholders in the normal format (${…​}) are resolved by using the effective Environment for the supplied application name, profile, and label. +In this way, the resource endpoint is tightly integrated with the environment endpoints. +Consider the following example for a GIT or SVN repository:

    application.yml
    +nginx.conf

    where nginx.conf looks like this:

    server {
    +    listen              80;
    +    server_name         ${nginx.server.name};
    +}

    and application.yml like this:

    nginx:
    +  server:
    +    name: example.com
    +---
    +spring:
    +  profiles: development
    +nginx:
    +  server:
    +    name: develop.com

    The /foo/default/master/nginx.conf resource might be as follows:

    server {
    +    listen              80;
    +    server_name         example.com;
    +}

    and /foo/development/master/nginx.conf like this:

    server {
    +    listen              80;
    +    server_name         develop.com;
    +}
    [Note]Note

    As with the source files for environment configuration, the profile is used to resolve the file name. +So, if you want a profile-specific file, /*/development/*/logback.xml can be resolved by a file called logback-development.xml (in preference to logback.xml).

    [Note]Note

    If you do not want to supply the label and let the server use the default label, you can supply a useDefaultLabel request parameter. +So, the preceding example for the default profile could be /foo/default/nginx.conf?useDefaultLabel.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__single_sign_on_2.html b/Greenwich.SR5/multi/multi__single_sign_on_2.html new file mode 100644 index 00000000..8f4421cf --- /dev/null +++ b/Greenwich.SR5/multi/multi__single_sign_on_2.html @@ -0,0 +1,11 @@ + + + 85. Single Sign On

    85. Single Sign On

    [Note]Note

    All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +Spring Boot user guide.

    This project provides automatic binding from CloudFoundry service +credentials to the Spring Boot features. If you have a CloudFoundry +service called "sso", for instance, with credentials containing +"client_id", "client_secret" and "auth_domain", it will bind +automatically to the Spring OAuth2 client that you enable with +@EnableOAuth2Sso (from Spring Boot). The name of the service can be +parameterized using spring.oauth2.sso.serviceId.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__span_lifecycle.html b/Greenwich.SR5/multi/multi__span_lifecycle.html new file mode 100644 index 00000000..672786a7 --- /dev/null +++ b/Greenwich.SR5/multi/multi__span_lifecycle.html @@ -0,0 +1,62 @@ + + + 58. Span lifecycle

    58. Span lifecycle

    You can do the following operations on the Span by means of brave.Tracer:

    • start: When you start a span, its name is assigned and the start timestamp is recorded.
    • close: The span gets finished (the end time of the span is recorded) and, if the span is sampled, it is eligible for collection (for example, to Zipkin).
    • continue: A new instance of span is created. +It is a copy of the one that it continues.
    • detach: The span does not get stopped or closed. +It only gets removed from the current thread.
    • create with explicit parent: You can create a new span and set an explicit parent for it.
    [Tip]Tip

    Spring Cloud Sleuth creates an instance of Tracer for you. In order to use it, you can autowire it.

    58.1 Creating and finishing spans

    You can manually create spans by using the Tracer, as shown in the following example:

    // Start a span. If there was a span present in this thread it will become
    +// the `newSpan`'s parent.
    +Span newSpan = this.tracer.nextSpan().name("calculateTax");
    +try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(newSpan.start())) {
    +	// ...
    +	// You can tag a span
    +	newSpan.tag("taxValue", taxValue);
    +	// ...
    +	// You can log an event on a span
    +	newSpan.annotate("taxCalculated");
    +}
    +finally {
    +	// Once done remember to finish the span. This will allow collecting
    +	// the span to send it to Zipkin
    +	newSpan.finish();
    +}

    In the preceding example, we could see how to create a new instance of the span. +If there is already a span in this thread, it becomes the parent of the new span.

    [Important]Important

    Always clean after you create a span. Also, always finish any span that you want to send to Zipkin.

    [Important]Important

    If your span contains a name greater than 50 chars, that name is truncated to 50 chars. +Your names have to be explicit and concrete. Big names lead to latency issues and sometimes even exceptions.

    58.2 Continuing Spans

    Sometimes, you do not want to create a new span but you want to continue one. An example of such a +situation might be as follows:

    • AOP: If there was already a span created before an aspect was reached, you might not want to create a new span.
    • Hystrix: Executing a Hystrix command is most likely a logical part of the current processing. +It is in fact merely a technical implementation detail that you would not necessarily want to reflect in tracing as a separate being.

    To continue a span, you can use brave.Tracer, as shown in the following example:

    // let's assume that we're in a thread Y and we've received
    +// the `initialSpan` from thread X
    +Span continuedSpan = this.tracer.toSpan(newSpan.context());
    +try {
    +	// ...
    +	// You can tag a span
    +	continuedSpan.tag("taxValue", taxValue);
    +	// ...
    +	// You can log an event on a span
    +	continuedSpan.annotate("taxCalculated");
    +}
    +finally {
    +	// Once done remember to flush the span. That means that
    +	// it will get reported but the span itself is not yet finished
    +	continuedSpan.flush();
    +}

    58.3 Creating a Span with an explicit Parent

    You might want to start a new span and provide an explicit parent of that span. +Assume that the parent of a span is in one thread and you want to start a new span in another thread. +In Brave, whenever you call nextSpan(), it creates a span in reference to the span that is currently in scope. +You can put the span in scope and then call nextSpan(), as shown in the following example:

    // let's assume that we're in a thread Y and we've received
    +// the `initialSpan` from thread X. `initialSpan` will be the parent
    +// of the `newSpan`
    +Span newSpan = null;
    +try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(initialSpan)) {
    +	newSpan = this.tracer.nextSpan().name("calculateCommission");
    +	// ...
    +	// You can tag a span
    +	newSpan.tag("commissionValue", commissionValue);
    +	// ...
    +	// You can log an event on a span
    +	newSpan.annotate("commissionCalculated");
    +}
    +finally {
    +	// Once done remember to finish the span. This will allow collecting
    +	// the span to send it to Zipkin. The tags and events set on the
    +	// newSpan will not be present on the parent
    +	if (newSpan != null) {
    +		newSpan.finish();
    +	}
    +}
    [Important]Important

    After creating such a span, you must finish it. Otherwise it is not reported (for example, to Zipkin).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_bus.html b/Greenwich.SR5/multi/multi__spring_cloud_bus.html new file mode 100644 index 00000000..84f2fd45 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_bus.html @@ -0,0 +1,8 @@ + + + Part VII. Spring Cloud Bus

    Part VII. Spring Cloud Bus

    Spring Cloud Bus links the nodes of a distributed system with a lightweight message +broker. This broker can then be used to broadcast state changes (such as configuration +changes) or other management instructions. A key idea is that the bus is like a +distributed actuator for a Spring Boot application that is scaled out. However, it can +also be used as a communication channel between apps. This project provides starters for +either an AMQP broker or Kafka as the transport.

    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_commons_common_abstractions.html b/Greenwich.SR5/multi/multi__spring_cloud_commons_common_abstractions.html new file mode 100644 index 00000000..7c29b1c9 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_commons_common_abstractions.html @@ -0,0 +1,344 @@ + + + 3. Spring Cloud Commons: Common Abstractions

    3. Spring Cloud Commons: Common Abstractions

    Patterns such as service discovery, load balancing, and circuit breakers lend themselves to a common abstraction layer that can be consumed by all Spring Cloud clients, independent of the implementation (for example, discovery with Eureka or Consul).

    3.1 @EnableDiscoveryClient

    Spring Cloud Commons provides the @EnableDiscoveryClient annotation. +This looks for implementations of the DiscoveryClient interface with META-INF/spring.factories. +Implementations of the Discovery Client add a configuration class to spring.factories under the org.springframework.cloud.client.discovery.EnableDiscoveryClient key. +Examples of DiscoveryClient implementations include Spring Cloud Netflix Eureka, Spring Cloud Consul Discovery, and Spring Cloud Zookeeper Discovery.

    By default, implementations of DiscoveryClient auto-register the local Spring Boot server with the remote discovery server. +This behavior can be disabled by setting autoRegister=false in @EnableDiscoveryClient.

    [Note]Note

    @EnableDiscoveryClient is no longer required. +You can put a DiscoveryClient implementation on the classpath to cause the Spring Boot application to register with the service discovery server.

    3.1.1 Health Indicator

    Commons creates a Spring Boot HealthIndicator that DiscoveryClient implementations can participate in by implementing DiscoveryHealthIndicator. +To disable the composite HealthIndicator, set spring.cloud.discovery.client.composite-indicator.enabled=false. +A generic HealthIndicator based on DiscoveryClient is auto-configured (DiscoveryClientHealthIndicator). +To disable it, set spring.cloud.discovery.client.health-indicator.enabled=false. +To disable the description field of the DiscoveryClientHealthIndicator, set spring.cloud.discovery.client.health-indicator.include-description=false. +Otherwise, it can bubble up as the description of the rolled up HealthIndicator.

    3.1.2 Ordering DiscoveryClient instances

    DiscoveryClient interface extends Ordered. This is useful when using multiple discovery + clients, as it allows you to define the order of the returned discovery clients, similar to +how you can order the beans loaded by a Spring application. By default, the order of any DiscoveryClient is set to +0. If you want to set a different order for your custom DiscoveryClient implementations, you just need to override +the getOrder() method so that it returns the value that is suitable for your setup. Apart from this, you can use +properties to set the order of the DiscoveryClient +implementations provided by Spring Cloud, among others ConsulDiscoveryClient, EurekaDiscoveryClient and +ZookeeperDiscoveryClient. In order to do it, you just need to set the +spring.cloud.{clientIdentifier}.discovery.order (or eureka.client.order for Eureka) property to the desired value.

    3.2 ServiceRegistry

    Commons now provides a ServiceRegistry interface that provides methods such as register(Registration) and deregister(Registration), which let you provide custom registered services. +Registration is a marker interface.

    The following example shows the ServiceRegistry in use:

    @Configuration
    +@EnableDiscoveryClient(autoRegister=false)
    +public class MyConfiguration {
    +    private ServiceRegistry registry;
    +
    +    public MyConfiguration(ServiceRegistry registry) {
    +        this.registry = registry;
    +    }
    +
    +    // called through some external process, such as an event or a custom actuator endpoint
    +    public void register() {
    +        Registration registration = constructRegistration();
    +        this.registry.register(registration);
    +    }
    +}

    Each ServiceRegistry implementation has its own Registry implementation.

    • ZookeeperRegistration used with ZookeeperServiceRegistry
    • EurekaRegistration used with EurekaServiceRegistry
    • ConsulRegistration used with ConsulServiceRegistry

    If you are using the ServiceRegistry interface, you are going to need to pass the +correct Registry implementation for the ServiceRegistry implementation you +are using.

    3.2.1 ServiceRegistry Auto-Registration

    By default, the ServiceRegistry implementation auto-registers the running service. +To disable that behavior, you can set: +* @EnableDiscoveryClient(autoRegister=false) to permanently disable auto-registration. +* spring.cloud.service-registry.auto-registration.enabled=false to disable the behavior through configuration.

    ServiceRegistry Auto-Registration Events

    There are two events that will be fired when a service auto-registers. The first event, called +InstancePreRegisteredEvent, is fired before the service is registered. The second +event, called InstanceRegisteredEvent, is fired after the service is registered. You can register an +ApplicationListener(s) to listen to and react to these events.

    [Note]Note

    These events will not be fired if spring.cloud.service-registry.auto-registration.enabled is set to false.

    3.2.2 Service Registry Actuator Endpoint

    Spring Cloud Commons provides a /service-registry actuator endpoint. +This endpoint relies on a Registration bean in the Spring Application Context. +Calling /service-registry with GET returns the status of the Registration. +Using POST to the same endpoint with a JSON body changes the status of the current Registration to the new value. +The JSON body has to include the status field with the preferred value. +Please see the documentation of the ServiceRegistry implementation you use for the allowed values when updating the status and the values returned for the status. +For instance, Eureka’s supported statuses are UP, DOWN, OUT_OF_SERVICE, and UNKNOWN.

    3.3 Spring RestTemplate as a Load Balancer Client

    RestTemplate can be automatically configured to use a Load-balancer client under the hood. +To create a load-balanced RestTemplate, create a RestTemplate @Bean and use the @LoadBalanced qualifier, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +    @LoadBalanced
    +    @Bean
    +    RestTemplate restTemplate() {
    +        return new RestTemplate();
    +    }
    +}
    +
    +public class MyClass {
    +    @Autowired
    +    private RestTemplate restTemplate;
    +
    +    public String doOtherStuff() {
    +        String results = restTemplate.getForObject("http://stores/stores", String.class);
    +        return results;
    +    }
    +}
    [Caution]Caution

    A RestTemplate bean is no longer created through auto-configuration. +Individual applications must create it.

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The Ribbon client is used to create a full physical address. +See RibbonAutoConfiguration for details of how the RestTemplate is set up.

    [Important]Important

    In order to use a load-balanced RestTemplate, you need to have a load-balancer implementation in your classpath. +The recommended implementation is BlockingLoadBalancerClient +- add org.springframework.cloud:spring-cloud-loadbalancer in order to use it. +The +RibbonLoadBalancerClient also can be used, but it’s now under maintenance and we do not recommend adding it to new projects.

    [Warning]Warning

    If you want to use BlockingLoadBalancerClient, make sure you do not have +RibbonLoadBalancerClient in the project classpath, as for backward compatibility reasons, it will be used by default.

    3.4 Spring WebClient as a Load Balancer Client

    WebClient can be automatically configured to use a load-balancer client. +To create a load-balanced WebClient, create a WebClient.Builder @Bean and use the @LoadBalanced qualifier, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +	@Bean
    +	@LoadBalanced
    +	public WebClient.Builder loadBalancedWebClientBuilder() {
    +		return WebClient.builder();
    +	}
    +}
    +
    +public class MyClass {
    +    @Autowired
    +    private WebClient.Builder webClientBuilder;
    +
    +    public Mono<String> doOtherStuff() {
    +        return webClientBuilder.build().get().uri("http://stores/stores")
    +        				.retrieve().bodyToMono(String.class);
    +    }
    +}

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The Ribbon client is used to create a full physical address.

    [Important]Important

    If you want to use a @LoadBalanced WebClient.Builder, you need to have a loadbalancer +implementation in the classpath. It is recommended that you add the +org.springframework.cloud:spring-cloud-loadbalancer dependency to your project. +Then, ReactiveLoadBalancer will be used underneath. +Alternatively, this functionality will also work with spring-cloud-starter-netflix-ribbon, but the request +will be handled by a non-reactive LoadBalancerClient under the hood. Additionally, +spring-cloud-starter-netflix-ribbon is already in maintenance mode, so we do not recommned +adding it to new projects.

    [Tip]Tip

    The ReactorLoadBalancer used underneath supports caching. If cacheManager is detected, +cached version of ServiceInstanceSupplier will be used. If not, we will retrieve instances +from discovery service without caching them. We recommend enabling caching in your project +if you use ReactiveLoadBalancer.

    3.4.1 Retrying Failed Requests

    A load-balanced RestTemplate can be configured to retry failed requests. +By default, this logic is disabled. +You can enable it by adding Spring Retry to your application’s classpath. +The load-balanced RestTemplate honors some of the Ribbon configuration values related to retrying failed requests. +You can use client.ribbon.MaxAutoRetries, client.ribbon.MaxAutoRetriesNextServer, and client.ribbon.OkToRetryOnAllOperations properties. +If you would like to disable the retry logic with Spring Retry on the classpath, you can set spring.cloud.loadbalancer.retry.enabled=false. +See the Ribbon documentation for a description of what these properties do.

    If you would like to implement a BackOffPolicy in your retries, you need to create a bean of type LoadBalancedRetryFactory and override the createBackOffPolicy method:

    @Configuration
    +public class MyConfiguration {
    +    @Bean
    +    LoadBalancedRetryFactory retryFactory() {
    +        return new LoadBalancedRetryFactory() {
    +            @Override
    +            public BackOffPolicy createBackOffPolicy(String service) {
    +        		return new ExponentialBackOffPolicy();
    +        	}
    +        };
    +    }
    +}
    [Note]Note

    client in the preceding examples should be replaced with your Ribbon client’s name.

    If you want to add one or more RetryListener implementations to your retry functionality, you need to +create a bean of type LoadBalancedRetryListenerFactory and return the RetryListener array +you would like to use for a given service, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +    @Bean
    +    LoadBalancedRetryListenerFactory retryListenerFactory() {
    +        return new LoadBalancedRetryListenerFactory() {
    +            @Override
    +            public RetryListener[] createRetryListeners(String service) {
    +                return new RetryListener[]{new RetryListener() {
    +                    @Override
    +                    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
    +                        //TODO Do you business...
    +                        return true;
    +                    }
    +
    +                    @Override
    +                     public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
    +                        //TODO Do you business...
    +                    }
    +
    +                    @Override
    +                    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
    +                        //TODO Do you business...
    +                    }
    +                }};
    +            }
    +        };
    +    }
    +}

    3.5 Multiple RestTemplate objects

    If you want a RestTemplate that is not load-balanced, create a RestTemplate bean and inject it. +To access the load-balanced RestTemplate, use the @LoadBalanced qualifier when you create your @Bean, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +    @LoadBalanced
    +    @Bean
    +    RestTemplate loadBalanced() {
    +        return new RestTemplate();
    +    }
    +
    +    @Primary
    +    @Bean
    +    RestTemplate restTemplate() {
    +        return new RestTemplate();
    +    }
    +}
    +
    +public class MyClass {
    +@Autowired
    +private RestTemplate restTemplate;
    +
    +    @Autowired
    +    @LoadBalanced
    +    private RestTemplate loadBalanced;
    +
    +    public String doOtherStuff() {
    +        return loadBalanced.getForObject("http://stores/stores", String.class);
    +    }
    +
    +    public String doStuff() {
    +        return restTemplate.getForObject("https://example.com", String.class);
    +    }
    +}
    [Important]Important

    Notice the use of the @Primary annotation on the plain RestTemplate declaration in the preceding example to disambiguate the unqualified @Autowired injection.

    [Tip]Tip

    If you see errors such as java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89, try injecting RestOperations or setting spring.aop.proxyTargetClass=true.

    3.6 Multiple WebClient Objects

    If you want a WebClient that is not load-balanced, create a WebClient bean and inject it. +To access the load-balanced WebClient, use the @LoadBalanced qualifier when you create your @Bean, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +    @LoadBalanced
    +    @Bean
    +    WebClient.Builder loadBalanced() {
    +        return WebClient.builder();
    +    }
    +
    +    @Primary
    +    @Bean
    +    WebClient.Builder webClient() {
    +        return WebClient.builder();
    +    }
    +}
    +
    +public class MyClass {
    +    @Autowired
    +    private WebClient.Builder webClientBuilder;
    +
    +    @Autowired
    +    @LoadBalanced
    +    private WebClient.Builder loadBalanced;
    +
    +    public Mono<String> doOtherStuff() {
    +        return loadBalanced.build().get().uri("http://stores/stores")
    +        				.retrieve().bodyToMono(String.class);
    +    }
    +
    +    public Mono<String> doStuff() {
    +        return webClientBuilder.build().get().uri("http://example.com")
    +        				.retrieve().bodyToMono(String.class);
    +    }
    +}

    3.7 Spring WebFlux WebClient as a Load Balancer Client

    3.7.1 Spring WebFlux WebClient with Reactive Load Balancer

    WebClient can be configured to use the ReactiveLoadBalancer. +If you add org.springframework.cloud:spring-cloud-loadbalancer to your project, + ReactorLoadBalancerExchangeFilterFunction is auto-configured if spring-webflux is on the classpath. +The following example shows how to configure a WebClient to use reactive load balancer under the hood:

    public class MyClass {
    +    @Autowired
    +    private ReactorLoadBalancerExchangeFilterFunction lbFunction;
    +
    +    public Mono<String> doOtherStuff() {
    +        return WebClient.builder().baseUrl("http://stores")
    +            .filter(lbFunction)
    +            .build()
    +            .get()
    +            .uri("/stores")
    +            .retrieve()
    +            .bodyToMono(String.class);
    +    }
    +}

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The ReactorLoadBalancerClient is used to create a full physical address.

    3.7.2 Spring WebFlux WebClient with non-reactive Load Balancer Client

    If you you don’t have org.springframework.cloud:spring-cloud-loadbalancer in your project, +but you do have spring-cloud-starter-netflix-ribbon, you can still use WebClient with LoadBalancerClient. LoadBalancerExchangeFilterFunction +will be auto-configured if spring-webflux is on the classpath. Please note, however, that this is +uses a non-reactive client under the hood. +The following example shows how to configure a WebClient to use load balancer:

    public class MyClass {
    +    @Autowired
    +    private LoadBalancerExchangeFilterFunction lbFunction;
    +
    +    public Mono<String> doOtherStuff() {
    +        return WebClient.builder().baseUrl("http://stores")
    +            .filter(lbFunction)
    +            .build()
    +            .get()
    +            .uri("/stores")
    +            .retrieve()
    +            .bodyToMono(String.class);
    +    }
    +}

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The LoadBalancerClient is used to create a full physical address.

    WARN: This approach is now deprecated. +We suggest you use WebFlux with reactive Load-Balancer +instead.

    3.7.3 Passing your own Load-Balancer Client configuration

    You can also use the @LoadBalancerClient annotation to pass your own load-balancer client configuration, passing the name of the load-balancer client and the configuration class, like so:

    @Configuration
    +@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class)
    +public class MyConfiguration {
    +
    +	@Bean
    +	@LoadBalanced
    +	public WebClient.Builder loadBalancedWebClientBuilder() {
    +		return WebClient.builder();
    +	}
    +}

    It is also possible to pass together multiple configurations (for more than one load-balancer client) via the @LoadBalancerClients annotation, as shown below:

    @Configuration
    +@LoadBalancerClients({@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class), @LoadBalancerClient(value = "customers", configuration = CustomersLoadBalancerClientConfiguration.class)})
    +public class MyConfiguration {
    +
    +	@Bean
    +	@LoadBalanced
    +	public WebClient.Builder loadBalancedWebClientBuilder() {
    +		return WebClient.builder();
    +	}
    +}

    3.8 Ignore Network Interfaces

    Sometimes, it is useful to ignore certain named network interfaces so that they can be excluded from Service Discovery registration (for example, when running in a Docker container). +A list of regular expressions can be set to cause the desired network interfaces to be ignored. +The following configuration ignores the docker0 interface and all interfaces that start with veth:

    application.yml.  +

    spring:
    +  cloud:
    +    inetutils:
    +      ignoredInterfaces:
    +        - docker0
    +        - veth.*

    +

    You can also force the use of only specified network addresses by using a list of regular expressions, as shown in the following example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    inetutils:
    +      preferredNetworks:
    +        - 192.168
    +        - 10.0

    +

    You can also force the use of only site-local addresses, as shown in the following example: +.application.yml

    spring:
    +  cloud:
    +    inetutils:
    +      useOnlySiteLocalInterfaces: true

    See Inet4Address.html.isSiteLocalAddress() for more details about what constitutes a site-local address.

    3.9 HTTP Client Factories

    Spring Cloud Commons provides beans for creating both Apache HTTP clients (ApacheHttpClientFactory) and OK HTTP clients (OkHttpClientFactory). +The OkHttpClientFactory bean is created only if the OK HTTP jar is on the classpath. +In addition, Spring Cloud Commons provides beans for creating the connection managers used by both clients: ApacheHttpClientConnectionManagerFactory for the Apache HTTP client and OkHttpClientConnectionPoolFactory for the OK HTTP client. +If you would like to customize how the HTTP clients are created in downstream projects, you can provide your own implementation of these beans. +In addition, if you provide a bean of type HttpClientBuilder or OkHttpClient.Builder, the default factories use these builders as the basis for the builders returned to downstream projects. +You can also disable the creation of these beans by setting spring.cloud.httpclientfactories.apache.enabled or spring.cloud.httpclientfactories.ok.enabled to false.

    3.10 Enabled Features

    Spring Cloud Commons provides a /features actuator endpoint. +This endpoint returns features available on the classpath and whether they are enabled. +The information returned includes the feature type, name, version, and vendor.

    3.10.1 Feature types

    There are two types of 'features': abstract and named.

    Abstract features are features where an interface or abstract class is defined and that an implementation the creates, such as DiscoveryClient, LoadBalancerClient, or LockService. +The abstract class or interface is used to find a bean of that type in the context. +The version displayed is bean.getClass().getPackage().getImplementationVersion().

    Named features are features that do not have a particular class they implement, such as "Circuit Breaker", "API Gateway", "Spring Cloud Bus", and others. These features require a name and a bean type.

    3.10.2 Declaring features

    Any module can declare any number of HasFeature beans, as shown in the following examples:

    @Bean
    +public HasFeatures commonsFeatures() {
    +  return HasFeatures.abstractFeatures(DiscoveryClient.class, LoadBalancerClient.class);
    +}
    +
    +@Bean
    +public HasFeatures consulFeatures() {
    +  return HasFeatures.namedFeatures(
    +    new NamedFeature("Spring Cloud Bus", ConsulBusAutoConfiguration.class),
    +    new NamedFeature("Circuit Breaker", HystrixCommandAspect.class));
    +}
    +
    +@Bean
    +HasFeatures localFeatures() {
    +  return HasFeatures.builder()
    +      .abstractFeature(Foo.class)
    +      .namedFeature(new NamedFeature("Bar Feature", Bar.class))
    +      .abstractFeature(Baz.class)
    +      .build();
    +}

    Each of these beans should go in an appropriately guarded @Configuration.

    3.11 Spring Cloud Compatibility Verification

    Due to the fact that some users have problem with setting up Spring Cloud application, we’ve decided +to add a compatibility verification mechanism. It will break if your current setup is not compatible +with Spring Cloud requirements, together with a report, showing what exactly went wrong.

    At the moment we verify which version of Spring Boot is added to your classpath.

    Example of a report

    ***************************
    +APPLICATION FAILED TO START
    +***************************
    +
    +Description:
    +
    +Your project setup is incompatible with our requirements due to following reasons:
    +
    +- Spring Boot [2.1.0.RELEASE] is not compatible with this Spring Cloud release train
    +
    +
    +Action:
    +
    +Consider applying the following actions:
    +
    +- Change Spring Boot version to one of the following versions [1.2.x, 1.3.x] .
    +You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn].
    +If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section.

    In order to disable this feature, set spring.cloud.compatibility-verifier.enabled to false. +If you want to override the compatible Spring Boot versions, just set the +spring.cloud.compatibility-verifier.compatible-boot-versions property with a comma separated list +of compatible Spring Boot versions.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_config.html b/Greenwich.SR5/multi/multi__spring_cloud_config.html new file mode 100644 index 00000000..7a6f9216 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_config.html @@ -0,0 +1,7 @@ + + + Part II. Spring Cloud Config

    Part II. Spring Cloud Config

    Greenwich.SR5

    Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments. +The concepts on both client and server map identically to the Spring Environment and PropertySource abstractions, so they fit very well with Spring applications but can be used with any application running in any language. +As an application moves through the deployment pipeline from dev to test and into production, you can manage the configuration between those environments and be certain that applications have everything they need to run when they migrate. +The default implementation of the server storage backend uses git, so it easily supports labelled versions of configuration environments as well as being accessible to a wide range of tooling for managing the content. +It is easy to add alternative implementations and plug them in with Spring configuration.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_config_2.html b/Greenwich.SR5/multi/multi__spring_cloud_config_2.html new file mode 100644 index 00000000..9ba0115d --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_config_2.html @@ -0,0 +1,35 @@ + + + 162. Spring Cloud Config

    162. Spring Cloud Config

    Spring Cloud GCP makes it possible to use the Google Runtime Configuration API as a Spring Cloud Config server to remotely store your application configuration data.

    The Spring Cloud GCP Config support is provided via its own Spring Boot starter. +It enables the use of the Google Runtime Configuration API as a source for Spring Boot configuration properties.

    [Note]Note

    The Google Cloud Runtime Configuration service is in beta status.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-config</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-config'
    +}

    162.1 Configuration

    The following parameters are configurable in Spring Cloud GCP Config:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.config.enabled

    Enables the Config client

    No

    false

    spring.cloud.gcp.config.name

    Name of your application

    No

    Value of the spring.application.name property. +If none, application

    spring.cloud.gcp.config.profile

    Active profile

    No

    Value of the spring.profiles.active property. +If more than a single profile, last one is chosen

    spring.cloud.gcp.config.timeout-millis

    Timeout in milliseconds for connecting to the Google Runtime Configuration API

    No

    60000

    spring.cloud.gcp.config.project-id

    GCP project ID where the Google Runtime Configuration API is hosted

    No

     

    spring.cloud.gcp.config.credentials.location

    OAuth2 credentials for authenticating with the Google Runtime Configuration API

    No

     

    spring.cloud.gcp.config.credentials.encoded-key

    Base64-encoded OAuth2 credentials for authenticating with the Google Runtime Configuration API

    No

     

    spring.cloud.gcp.config.credentials.scopes

    OAuth2 scope for Spring Cloud GCP Config credentials

    No

    https://www.googleapis.com/auth/cloudruntimeconfig

    [Note]Note

    These properties should be specified in a bootstrap.yml/bootstrap.properties file, rather than the usual applications.yml/application.properties.

    [Note]Note

    Core properties, as described in Spring Cloud GCP Core Module, do not apply to Spring Cloud GCP Config.

    162.2 Quick start

    1. Create a configuration in the Google Runtime Configuration API that is called ${spring.application.name}_${spring.profiles.active}. +In other words, if spring.application.name is myapp and spring.profiles.active is prod, the configuration should be called myapp_prod.

      In order to do that, you should have the Google Cloud SDK installed, own a Google Cloud Project and run the following command:

    gcloud init # if this is your first Google Cloud SDK run.
    +gcloud beta runtime-config configs create myapp_prod
    +gcloud beta runtime-config configs variables set myapp.queue-size 25 --config-name myapp_prod
    1. Configure your bootstrap.properties file with your application’s configuration data:

      spring.application.name=myapp
      +spring.profiles.active=prod
    2. Add the @ConfigurationProperties annotation to a Spring-managed bean:

      @Component
      +@ConfigurationProperties("myapp")
      +public class SampleConfig {
      +
      +  private int queueSize;
      +
      +  public int getQueueSize() {
      +    return this.queueSize;
      +  }
      +
      +  public void setQueueSize(int queueSize) {
      +    this.queueSize = queueSize;
      +  }
      +}

    When your Spring application starts, the queueSize field value will be set to 25 for the above SampleConfig bean.

    162.3 Refreshing the configuration at runtime

    Spring Cloud provides support to have configuration parameters be reloadable with the POST request to /actuator/refresh endpoint.

    1. Add the Spring Boot Actuator dependency:

    Maven coordinates:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-actuator</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator'
    +}
    1. Add @RefreshScope to your Spring configuration class to have parameters be reloadable at runtime.
    2. Add management.endpoints.web.exposure.include=refresh to your application.properties to allow unrestricted access to /actuator/refresh.
    3. Update a property with gcloud:

      $ gcloud beta runtime-config configs variables set \
      +  myapp.queue_size 200 \
      +  --config-name myapp_prod
    4. Send a POST request to the refresh endpoint:

      $ curl -XPOST https://myapp.host.com/actuator/refresh

    162.4 Sample

    A sample application and a codelab are available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_config_client.html b/Greenwich.SR5/multi/multi__spring_cloud_config_client.html new file mode 100644 index 00000000..3a0ad2dd --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_config_client.html @@ -0,0 +1,83 @@ + + + 10. Spring Cloud Config Client

    10. Spring Cloud Config Client

    A Spring Boot application can take immediate advantage of the Spring Config Server (or other external property sources provided by the application developer). +It also picks up some additional useful features related to Environment change events.

    10.1 Config First Bootstrap

    The default behavior for any application that has the Spring Cloud Config Client on the classpath is as follows: +When a config client starts, it binds to the Config Server (through the spring.cloud.config.uri bootstrap configuration property) and initializes Spring Environment with remote property sources.

    The net result of this behavior is that all client applications that want to consume the Config Server need a bootstrap.yml (or an environment variable) with the server address set in spring.cloud.config.uri (it defaults to "http://localhost:8888").

    10.2 Discovery First Bootstrap

    If you use a DiscoveryClient implementation, such as Spring Cloud Netflix and Eureka Service Discovery or Spring Cloud Consul, you can have the Config Server register with the Discovery Service. +However, in the default Config First mode, clients cannot take advantage of the registration.

    If you prefer to use DiscoveryClient to locate the Config Server, you can do so by setting spring.cloud.config.discovery.enabled=true (the default is false). +The net result of doing so is that client applications all need a bootstrap.yml (or an environment variable) with the appropriate discovery configuration. +For example, with Spring Cloud Netflix, you need to define the Eureka server address (for example, in eureka.client.serviceUrl.defaultZone). +The price for using this option is an extra network round trip on startup, to locate the service registration. +The benefit is that, as long as the Discovery Service is a fixed point, the Config Server can change its coordinates. +The default service ID is configserver, but you can change that on the client by setting spring.cloud.config.discovery.serviceId (and on the server, in the usual way for a service, such as by setting spring.application.name).

    The discovery client implementations all support some kind of metadata map (for example, we have eureka.instance.metadataMap for Eureka). +Some additional properties of the Config Server may need to be configured in its service registration metadata so that clients can connect correctly. +If the Config Server is secured with HTTP Basic, you can configure the credentials as user and password. +Also, if the Config Server has a context path, you can set configPath. +For example, the following YAML file is for a Config Server that is a Eureka client:

    bootstrap.yml.  +

    eureka:
    +  instance:
    +    ...
    +    metadataMap:
    +      user: osufhalskjrtl
    +      password: lviuhlszvaorhvlo5847
    +      configPath: /config

    +

    10.3 Config Client Fail Fast

    In some cases, you may want to fail startup of a service if it cannot connect to the Config Server. +If this is the desired behavior, set the bootstrap configuration property spring.cloud.config.fail-fast=true to make the client halt with an Exception.

    10.4 Config Client Retry

    If you expect that the config server may occasionally be unavailable when your application starts, you can make it keep trying after a failure. +First, you need to set spring.cloud.config.fail-fast=true. +Then you need to add spring-retry and spring-boot-starter-aop to your classpath. +The default behavior is to retry six times with an initial backoff interval of 1000ms and an exponential multiplier of 1.1 for subsequent backoffs. +You can configure these properties (and others) by setting the spring.cloud.config.retry.* configuration properties.

    [Tip]Tip

    To take full control of the retry behavior, add a @Bean of type RetryOperationsInterceptor with an ID of configServerRetryInterceptor. +Spring Retry has a RetryInterceptorBuilder that supports creating one.

    10.5 Locating Remote Configuration Resources

    The Config Service serves property sources from /{application}/{profile}/{label}, where the default bindings in the client app are as follows:

    • "name" = ${spring.application.name}
    • "profile" = ${spring.profiles.active} (actually Environment.getActiveProfiles())
    • "label" = "master"
    [Note]Note

    When setting the property ${spring.application.name} do not prefix your app name with the reserved word application- to prevent issues resolving the correct property source.

    You can override all of them by setting spring.cloud.config.* (where * is name, profile or label). +The label is useful for rolling back to previous versions of configuration. +With the default Config Server implementation, it can be a git label, branch name, or commit ID. +Label can also be provided as a comma-separated list. +In that case, the items in the list are tried one by one until one succeeds. +This behavior can be useful when working on a feature branch. +For instance, you might want to align the config label with your branch but make it optional (in that case, use spring.cloud.config.label=myfeature,develop).

    10.6 Specifying Multiple Urls for the Config Server

    To ensure high availability when you have multiple instances of Config Server deployed and expect one or more instances to be unavailable from time to time, you can either specify multiple URLs (as a comma-separated list under the spring.cloud.config.uri property) or have all your instances register in a Service Registry like Eureka ( if using Discovery-First Bootstrap mode ). Note that doing so ensures high availability only when the Config Server is not running (that is, when the application has exited) or when a connection timeout has occurred. For example, if the Config Server returns a 500 (Internal Server Error) response or the Config Client receives a 401 from the Config Server (due to bad credentials or other causes), the Config Client does not try to fetch properties from other URLs. An error of that kind indicates a user issue rather than an availability problem.

    If you use HTTP basic security on your Config Server, it is currently possible to support per-Config Server auth credentials only if you embed the credentials in each URL you specify under the spring.cloud.config.uri property. If you use any other kind of security mechanism, you cannot (currently) support per-Config Server authentication and authorization.

    10.7 Configuring Timeouts

    If you want to configure timeout thresholds:

    • Read timeouts can be configured by using the property spring.cloud.config.request-read-timeout.
    • Connection timeouts can be configured by using the property spring.cloud.config.request-connect-timeout.

    10.8 Security

    If you use HTTP Basic security on the server, clients need to know the password (and username if it is not the default). +You can specify the username and password through the config server URI or via separate username and password properties, as shown in the following example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +     uri: https://user:secret@myconfig.mycompany.com

    +

    The following example shows an alternate way to pass the same information:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +     uri: https://myconfig.mycompany.com
    +     username: user
    +     password: secret

    +

    The spring.cloud.config.password and spring.cloud.config.username values override anything that is provided in the URI.

    If you deploy your apps on Cloud Foundry, the best way to provide the password is through service credentials (such as in the URI, since it does not need to be in a config file). +The following example works locally and for a user-provided service on Cloud Foundry named configserver:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +     uri: ${vcap.services.configserver.credentials.uri:http://user:password@localhost:8888}

    +

    If you use another form of security, you might need to provide a RestTemplate to the ConfigServicePropertySourceLocator (for example, by grabbing it in the bootstrap context and injecting it).

    10.8.1 Health Indicator

    The Config Client supplies a Spring Boot Health Indicator that attempts to load configuration from the Config Server. +The health indicator can be disabled by setting health.config.enabled=false. +The response is also cached for performance reasons. +The default cache time to live is 5 minutes. +To change that value, set the health.config.time-to-live property (in milliseconds).

    10.8.2 Providing A Custom RestTemplate

    In some cases, you might need to customize the requests made to the config server from the client. +Typically, doing so involves passing special Authorization headers to authenticate requests to the server. +To provide a custom RestTemplate:

    1. Create a new configuration bean with an implementation of PropertySourceLocator, as shown in the following example:

    CustomConfigServiceBootstrapConfiguration.java.  +

    @Configuration
    +public class CustomConfigServiceBootstrapConfiguration {
    +    @Bean
    +    public ConfigServicePropertySourceLocator configServicePropertySourceLocator() {
    +        ConfigClientProperties clientProperties = configClientProperties();
    +       ConfigServicePropertySourceLocator configServicePropertySourceLocator =  new ConfigServicePropertySourceLocator(clientProperties);
    +        configServicePropertySourceLocator.setRestTemplate(customRestTemplate(clientProperties));
    +        return configServicePropertySourceLocator;
    +    }
    +}

    +

    1. In resources/META-INF, create a file called +spring.factories and specify your custom configuration, as shown in the following example:

    spring.factories.  +

    org.springframework.cloud.bootstrap.BootstrapConfiguration = com.my.config.client.CustomConfigServiceBootstrapConfiguration

    +

    10.8.3 Vault

    When using Vault as a backend to your config server, the client needs to supply a token for the server to retrieve values from Vault. +This token can be provided within the client by setting spring.cloud.config.token +in bootstrap.yml, as shown in the following example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +      token: YourVaultToken

    +

    10.9 Nested Keys In Vault

    Vault supports the ability to nest keys in a value stored in Vault, as shown in the following example:

    echo -n '{"appA": {"secret": "appAsecret"}, "bar": "baz"}' | vault write secret/myapp -

    This command writes a JSON object to your Vault. +To access these values in Spring, you would use the traditional dot(.) annotation, as shown in the following example

    @Value("${appA.secret}")
    +String name = "World";

    The preceding code would sets the value of the name variable to appAsecret.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_config_server.html b/Greenwich.SR5/multi/multi__spring_cloud_config_server.html new file mode 100644 index 00000000..314a4090 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_config_server.html @@ -0,0 +1,493 @@ + + + 5. Spring Cloud Config Server

    5. Spring Cloud Config Server

    Spring Cloud Config Server provides an HTTP resource-based API for external configuration (name-value pairs or equivalent YAML content). +The server is embeddable in a Spring Boot application, by using the @EnableConfigServer annotation. +Consequently, the following application is a config server:

    ConfigServer.java.  +

    @SpringBootApplication
    +@EnableConfigServer
    +public class ConfigServer {
    +  public static void main(String[] args) {
    +    SpringApplication.run(ConfigServer.class, args);
    +  }
    +}

    +

    Like all Spring Boot applications, it runs on port 8080 by default, but you can switch it to the more conventional port 8888 in various ways. +The easiest, which also sets a default configuration repository, is by launching it with spring.config.name=configserver (there is a configserver.yml in the Config Server jar). +Another is to use your own application.properties, as shown in the following example:

    application.properties.  +

    server.port: 8888
    +spring.cloud.config.server.git.uri: file://${user.home}/config-repo

    +

    where ${user.home}/config-repo is a git repository containing YAML and properties files.

    [Note]Note

    On Windows, you need an extra "/" in the file URL if it is absolute with a drive prefix (for example,file:///${user.home}/config-repo).

    [Tip]Tip

    The following listing shows a recipe for creating the git repository in the preceding example:

    $ cd $HOME
    +$ mkdir config-repo
    +$ cd config-repo
    +$ git init .
    +$ echo info.foo: bar > application.properties
    +$ git add -A .
    +$ git commit -m "Add application.properties"
    [Warning]Warning

    Using the local filesystem for your git repository is intended for testing only. +You should use a server to host your configuration repositories in production.

    [Warning]Warning

    The initial clone of your configuration repository can be quick and efficient if you keep only text files in it. +If you store binary files, especially large ones, you may experience delays on the first request for configuration or encounter out of memory errors in the server.

    5.1 Environment Repository

    Where should you store the configuration data for the Config Server? +The strategy that governs this behaviour is the EnvironmentRepository, serving Environment objects. +This Environment is a shallow copy of the domain from the Spring Environment (including propertySources as the main feature). +The Environment resources are parametrized by three variables:

    • {application}, which maps to spring.application.name on the client side.
    • {profile}, which maps to spring.profiles.active on the client (comma-separated list).
    • {label}, which is a server side feature labelling a "versioned" set of config files.

    Repository implementations generally behave like a Spring Boot application, loading configuration files from a spring.config.name equal to the {application} parameter, and spring.profiles.active equal to the {profiles} parameter. +Precedence rules for profiles are also the same as in a regular Spring Boot application: Active profiles take precedence over defaults, and, if there are multiple profiles, the last one wins (similar to adding entries to a Map).

    The following sample client application has this bootstrap configuration:

    bootstrap.yml.  +

    spring:
    +  application:
    +    name: foo
    +  profiles:
    +    active: dev,mysql

    +

    (As usual with a Spring Boot application, these properties could also be set by environment variables or command line arguments).

    If the repository is file-based, the server creates an +Environment from application.yml (shared between all clients) and +foo.yml (with foo.yml taking precedence). +If the YAML files have documents inside them that point to Spring profiles, those are applied with higher precedence (in order of the profiles listed). +If there are profile-specific YAML (or properties) files, these are also applied with higher precedence than the defaults. +Higher precedence translates to a PropertySource listed earlier in the Environment. +(These same rules apply in a standalone Spring Boot application.)

    You can set spring.cloud.config.server.accept-empty to false so that Server would return a HTTP 404 status, if the application is not found.By default, this flag is set to true.

    5.1.1 Git Backend

    The default implementation of EnvironmentRepository uses a Git backend, which is very convenient for managing upgrades and physical environments and for auditing changes. +To change the location of the repository, you can set the spring.cloud.config.server.git.uri configuration property in the Config Server (for example in application.yml). +If you set it with a file: prefix, it should work from a local repository so that you can get started quickly and easily without a server. However, in that case, the server operates directly on the local repository without cloning it (it does not matter if it is not bare because the Config Server never makes changes to the "remote" repository). +To scale the Config Server up and make it highly available, you need to have all instances of the server pointing to the same repository, so only a shared file system would work. +Even in that case, it is better to use the ssh: protocol for a shared filesystem repository, so that the server can clone it and use a local working copy as a cache.

    This repository implementation maps the {label} parameter of the HTTP resource to a git label (commit id, branch name, or tag). +If the git branch or tag name contains a slash (/), then the label in the HTTP URL should instead be specified with the special string (_) (to avoid ambiguity with other URL paths). +For example, if the label is foo/bar, replacing the slash would result in the following label: foo(_)bar. +The inclusion of the special string (_) can also be applied to the {application} parameter. +If you use a command-line client such as curl, be careful with the brackets in the URL — you should escape them from the shell with single quotes ('').

    Skipping SSL Certificate Validation

    The configuration server’s validation of the Git server’s SSL certificate can be disabled by setting the git.skipSslValidation property to true (default is false).

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://example.com/my/repo
    +          skipSslValidation: true

    Setting HTTP Connection Timeout

    You can configure the time, in seconds, that the configuration server will wait to acquire an HTTP connection. Use the git.timeout property.

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://example.com/my/repo
    +          timeout: 4

    Placeholders in Git URI

    Spring Cloud Config Server supports a git repository URL with placeholders for the {application} and {profile} (and {label} if you need it, but remember that the label is applied as a git label anyway). +So you can support a one repository per application policy by using a structure similar to the following:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/myorg/{application}

    You can also support a one repository per profile policy by using a similar pattern but with +{profile}.

    Additionally, using the special string "(_)" within your {application} parameters can enable support for multiple +organizations, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/{application}

    where {application} is provided at request time in the following format: organization(_)application.

    Pattern Matching and Multiple Repositories

    Spring Cloud Config also includes support for more complex requirements with pattern +matching on the application and profile name. +The pattern format is a comma-separated list of {application}/{profile} names with wildcards (note that a pattern beginning with a wildcard may need to be quoted), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          repos:
    +            simple: https://github.com/simple/config-repo
    +            special:
    +              pattern: special*/dev*,*special*/dev*
    +              uri: https://github.com/special/config-repo
    +            local:
    +              pattern: local*
    +              uri: file:/home/configsvc/config-repo

    If {application}/{profile} does not match any of the patterns, it uses the default URI defined under spring.cloud.config.server.git.uri. +In the above example, for the simple repository, the pattern is simple/* (it only matches one application named simple in all profiles). The local repository matches all application names beginning with local in all profiles (the /* suffix is added automatically to any pattern that does not have a profile matcher).

    [Note]Note

    The one-liner short cut used in the simple example can be used only if the only property to be set is the URI. +If you need to set anything else (credentials, pattern, and so on) you need to use the full form.

    The pattern property in the repo is actually an array, so you can use a YAML array (or [0], [1], etc. suffixes in properties files) to bind to multiple patterns. +You may need to do so if you are going to run apps with multiple profiles, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          repos:
    +            development:
    +              pattern:
    +                - '*/development'
    +                - '*/staging'
    +              uri: https://github.com/development/config-repo
    +            staging:
    +              pattern:
    +                - '*/qa'
    +                - '*/production'
    +              uri: https://github.com/staging/config-repo
    [Note]Note

    Spring Cloud guesses that a pattern containing a profile that does not end in * implies that you actually want to match a list of profiles starting with this pattern (so */staging is a shortcut for ["*/staging", "*/staging,*"], and so on). +This is common where, for instance, you need to run applications in the development profile locally but also the cloud profile remotely.

    Every repository can also optionally store config files in sub-directories, and patterns to search for those directories can be specified as searchPaths. +The following example shows a config file at the top level:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          searchPaths: foo,bar*

    In the preceding example, the server searches for config files in the top level and in the foo/ sub-directory and also any sub-directory whose name begins with bar.

    By default, the server clones remote repositories when configuration +is first requested. +The server can be configured to clone the repositories at startup, as shown in the following top-level example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://git/common/config-repo.git
    +          repos:
    +            team-a:
    +                pattern: team-a-*
    +                cloneOnStart: true
    +                uri: https://git/team-a/config-repo.git
    +            team-b:
    +                pattern: team-b-*
    +                cloneOnStart: false
    +                uri: https://git/team-b/config-repo.git
    +            team-c:
    +                pattern: team-c-*
    +                uri: https://git/team-a/config-repo.git

    In the preceding example, the server clones team-a’s config-repo on startup, before it +accepts any requests. +All other repositories are not cloned until configuration from the repository is requested.

    [Note]Note

    Setting a repository to be cloned when the Config Server starts up can help to identify a misconfigured configuration source (such as an invalid repository URI) quickly, while the Config Server is starting up. +With cloneOnStart not enabled for a configuration source, the Config Server may start successfully with a misconfigured or invalid configuration source and not detect an error until an application requests configuration from that configuration source.

    Authentication

    To use HTTP basic authentication on the remote repository, add the username and password properties separately (not in the URL), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          username: trolley
    +          password: strongpassword

    If you do not use HTTPS and user credentials, SSH should also work out of the box when you store keys in the default directories (~/.ssh) and the URI points to an SSH location, such as git@github.com:configuration/cloud-configuration. +It is important that an entry for the Git server be present in the ~/.ssh/known_hosts file and that it is in ssh-rsa format. +Other formats (such as ecdsa-sha2-nistp256) are not supported. +To avoid surprises, you should ensure that only one entry is present in the known_hosts file for the Git server and that it matches the URL you provided to the config server. +If you use a hostname in the URL, you want to have exactly that (not the IP) in the known_hosts file. +The repository is accessed by using JGit, so any documentation you find on that should be applicable. +HTTPS proxy settings can be set in ~/.git/config or (in the same way as for any other JVM process) with +system properties (-Dhttps.proxyHost and -Dhttps.proxyPort).

    [Tip]Tip

    If you do not know where your ~/.git directory is, use git config --global to manipulate the settings (for example, git config --global http.sslVerify false).

    Authentication with AWS CodeCommit

    Spring Cloud Config Server also supports AWS CodeCommit authentication. +AWS CodeCommit uses an authentication helper when using Git from the command line. +This helper is not used with the JGit library, so a JGit CredentialProvider for AWS CodeCommit is created if the Git URI matches the AWS CodeCommit pattern. +AWS CodeCommit URIs follow this pattern://git-codecommit.${AWS_REGION}.amazonaws.com/${repopath}.

    If you provide a username and password with an AWS CodeCommit URI, they must be the AWS accessKeyId and secretAccessKey that provide access to the repository. +If you do not specify a username and password, the accessKeyId and secretAccessKey are retrieved by using the AWS Default Credential Provider Chain.

    If your Git URI matches the CodeCommit URI pattern (shown earlier), you must provide valid AWS credentials in the username and password or in one of the locations supported by the default credential provider chain. +AWS EC2 instances may use IAM Roles for EC2 Instances.

    [Note]Note

    The aws-java-sdk-core jar is an optional dependency. +If the aws-java-sdk-core jar is not on your classpath, the AWS Code Commit credential provider is not created, regardless of the git server URI.

    Git SSH configuration using properties

    By default, the JGit library used by Spring Cloud Config Server uses SSH configuration files such as ~/.ssh/known_hosts and /etc/ssh/ssh_config when connecting to Git repositories by using an SSH URI. +In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. +For those cases, SSH configuration can be set by using Java properties. +In order to activate property-based SSH configuration, the spring.cloud.config.server.git.ignoreLocalSshSettings property must be set to true, as shown in the following example:

      spring:
    +    cloud:
    +      config:
    +        server:
    +          git:
    +            uri: git@gitserver.com:team/repo1.git
    +            ignoreLocalSshSettings: true
    +            hostKey: someHostKey
    +            hostKeyAlgorithm: ssh-rsa
    +            privateKey: |
    +                         -----BEGIN RSA PRIVATE KEY-----
    +                         MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX
    +                         IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF
    +                         ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud
    +                         1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i
    +                         oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W
    +                         DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd
    +                         fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b
    +                         BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG
    +                         EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj
    +                         5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8
    +                         +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe
    +                         pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG
    +                         ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ
    +                         xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW
    +                         dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi
    +                         PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX
    +                         VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z
    +                         FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R
    +                         gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4
    +                         VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV
    +                         cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee
    +                         KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N
    +                         CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs
    +                         q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J
    +                         69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT
    +                         -----END RSA PRIVATE KEY-----

    The following table describes the SSH configuration properties.

    Table 5.1. SSH Configuration Properties

    Property NameRemarks

    ignoreLocalSshSettings

    If true, use property-based instead of file-based SSH config. Must be set at as spring.cloud.config.server.git.ignoreLocalSshSettings, not inside a repository definition.

    privateKey

    Valid SSH private key. Must be set if ignoreLocalSshSettings is true and Git URI is SSH format.

    hostKey

    Valid SSH host key. Must be set if hostKeyAlgorithm is also set.

    hostKeyAlgorithm

    One of ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521. Must be set if hostKey is also set.

    strictHostKeyChecking

    true or false. If false, ignore errors with host key.

    knownHostsFile

    Location of custom .known_hosts file.

    preferredAuthentications

    Override server authentication method order. This should allow for evading login prompts if server has keyboard-interactive authentication before the publickey method.


    Placeholders in Git Search Paths

    Spring Cloud Config Server also supports a search path with placeholders for the {application} and {profile} (and {label} if +you need it), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          searchPaths: '{application}'

    The preceding listing causes a search of the repository for files in the same name as the directory (as well as the top level). +Wildcards are also valid in a search path with placeholders (any matching directory is included in the search).

    Force pull in Git Repositories

    As mentioned earlier, Spring Cloud Config Server makes a clone of the remote git repository in case the local copy gets dirty (for example, +folder content changes by an OS process) such that Spring Cloud Config Server cannot update the local copy from remote repository.

    To solve this issue, there is a force-pull property that makes Spring Cloud Config Server force pull from the remote repository if the local copy is dirty, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          force-pull: true

    If you have a multiple-repositories configuration, you can configure the force-pull property per repository, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://git/common/config-repo.git
    +          force-pull: true
    +          repos:
    +            team-a:
    +                pattern: team-a-*
    +                uri: https://git/team-a/config-repo.git
    +                force-pull: true
    +            team-b:
    +                pattern: team-b-*
    +                uri: https://git/team-b/config-repo.git
    +                force-pull: true
    +            team-c:
    +                pattern: team-c-*
    +                uri: https://git/team-a/config-repo.git
    [Note]Note

    The default value for force-pull property is false.

    Deleting untracked branches in Git Repositories

    As Spring Cloud Config Server has a clone of the remote git repository +after check-outing branch to local repo (e.g fetching properties by label) it will keep this branch +forever or till the next server restart (which creates new local repo). +So there could be a case when remote branch is deleted but local copy of it is still available for fetching. +And if Spring Cloud Config Server client service starts with --spring.cloud.config.label=deletedRemoteBranch,master +it will fetch properties from deletedRemoteBranch local branch, but not from master.

    In order to keep local repository branches clean and up to remote - deleteUntrackedBranches property could be set. +It will make Spring Cloud Config Server force delete untracked branches from local repository. +Example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          deleteUntrackedBranches: true
    [Note]Note

    The default value for deleteUntrackedBranches property is false.

    Git Refresh Rate

    You can control how often the config server will fetch updated configuration data +from your Git backend by using spring.cloud.config.server.git.refreshRate. The +value of this property is specified in seconds. By default the value is 0, meaning +the config server will fetch updated configuration from the Git repo every time it +is requested.

    5.1.2 Version Control Backend Filesystem Use

    [Warning]Warning

    With VCS-based backends (git, svn), files are checked out or cloned to the local filesystem. +By default, they are put in the system temporary directory with a prefix of config-repo-. +On linux, for example, it could be /tmp/config-repo-<randomid>. +Some operating systems routinely clean out temporary directories. +This can lead to unexpected behavior, such as missing properties. +To avoid this problem, change the directory that Config Server uses by setting spring.cloud.config.server.git.basedir or spring.cloud.config.server.svn.basedir to a directory that does not reside in the system temp structure.

    5.1.3 File System Backend

    There is also a native profile in the Config Server that does not use Git but loads the config files from the local classpath or file system (any static URL you want to point to with spring.cloud.config.server.native.searchLocations). +To use the native profile, launch the Config Server with spring.profiles.active=native.

    [Note]Note

    Remember to use the file: prefix for file resources (the default without a prefix is usually the classpath). +As with any Spring Boot configuration, you can embed ${}-style environment placeholders, but remember that absolute paths in Windows require an extra / (for example, file:///${user.home}/config-repo).

    [Warning]Warning

    The default value of the searchLocations is identical to a local Spring Boot application (that is, [classpath:/, classpath:/config, +file:./, file:./config]). +This does not expose the application.properties from the server to all clients, because any property sources present in the server are removed before being sent to the client.

    [Tip]Tip

    A filesystem backend is great for getting started quickly and for testing. +To use it in production, you need to be sure that the file system is reliable and shared across all instances of the Config Server.

    The search locations can contain placeholders for {application}, {profile}, and {label}. +In this way, you can segregate the directories in the path and choose a strategy that makes sense for you (such as subdirectory per application or subdirectory per profile).

    If you do not use placeholders in the search locations, this repository also appends the {label} parameter of the HTTP resource to a suffix on the search path, so properties files are loaded from each search location and a subdirectory with the same name as the label (the labelled properties take precedence in the Spring Environment). +Thus, the default behaviour with no placeholders is the same as adding a search location ending with /{label}/. +For example, file:/tmp/config is the same as file:/tmp/config,file:/tmp/config/{label}. +This behavior can be disabled by setting spring.cloud.config.server.native.addLabelLocations=false.

    5.1.4 Vault Backend

    Spring Cloud Config Server also supports Vault as a backend.

    For more information on Vault, see the Vault quick start guide.

    To enable the config server to use a Vault backend, you can run your config server with the vault profile. +For example, in your config server’s application.properties, you can add spring.profiles.active=vault.

    By default, the config server assumes that your Vault server runs at http://127.0.0.1:8200. +It also assumes that the name of backend is secret and the key is application. +All of these defaults can be configured in your config server’s application.properties. +The following table describes configurable Vault properties:

    NameDefault Value

    host

    127.0.0.1

    port

    8200

    scheme

    http

    backend

    secret

    defaultKey

    application

    profileSeparator

    ,

    kvVersion

    1

    skipSslValidation

    false

    timeout

    5

    namespace

    null

    [Important]Important

    All of the properties in the preceding table must be prefixed with spring.cloud.config.server.vault or placed in the correct Vault section of a composite configuration.

    All configurable properties can be found in org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.

    Vault 0.10.0 introduced a versioned key-value backend (k/v backend version 2) that exposes a different API than earlier versions, it now requires a data/ between the mount path and the actual context path and wraps secrets in a data object. Setting kvVersion=2 will take this into account.

    Optionally, there is support for the Vault Enterprise X-Vault-Namespace header. To have it sent to Vault set the namespace property.

    With your config server running, you can make HTTP requests to the server to retrieve +values from the Vault backend. +To do so, you need a token for your Vault server.

    First, place some data in you Vault, as shown in the following example:

    $ vault kv put secret/application foo=bar baz=bam
    +$ vault kv put secret/myapp foo=myappsbar

    Second, make an HTTP request to your config server to retrieve the values, as shown in the following example:

    $ curl -X "GET" "http://localhost:8888/myapp/default" -H "X-Config-Token: yourtoken"

    You should see a response similar to the following:

    {
    +   "name":"myapp",
    +   "profiles":[
    +      "default"
    +   ],
    +   "label":null,
    +   "version":null,
    +   "state":null,
    +   "propertySources":[
    +      {
    +         "name":"vault:myapp",
    +         "source":{
    +            "foo":"myappsbar"
    +         }
    +      },
    +      {
    +         "name":"vault:application",
    +         "source":{
    +            "baz":"bam",
    +            "foo":"bar"
    +         }
    +      }
    +   ]
    +}

    Multiple Properties Sources

    When using Vault, you can provide your applications with multiple properties sources. +For example, assume you have written data to the following paths in Vault:

    secret/myApp,dev
    +secret/myApp
    +secret/application,dev
    +secret/application

    Properties written to secret/application are available to all applications using the Config Server. +An application with the name, myApp, would have any properties written to secret/myApp and secret/application available to it. +When myApp has the dev profile enabled, properties written to all of the above paths would be available to it, with properties in the first path in the list taking priority over the others.

    5.1.5 Accessing Backends Through a Proxy

    The configuration server can access a Git or Vault backend through an HTTP or HTTPS proxy. This behavior is controlled for either Git or Vault by settings under proxy.http and proxy.https. These settings are per repository, so if you are using a composite environment repository you must configure proxy settings for each backend in the composite individually. If using a network which requires separate proxy servers for HTTP and HTTPS URLs, you can configure both the HTTP and the HTTPS proxy settings for a single backend.

    The following table describes the proxy configuration properties for both HTTP and HTTPS proxies. All of these properties must be prefixed by proxy.http or proxy.https.

    Table 5.2. Proxy Configuration Properties

    Property NameRemarks

    host

    The host of the proxy.

    port

    The port with which to access the proxy.

    nonProxyHosts

    Any hosts which the configuration server should access outside the proxy. If values are provided for both proxy.http.nonProxyHosts and proxy.https.nonProxyHosts, the proxy.http value will be used.

    username

    The username with which to authenticate to the proxy. If values are provided for both proxy.http.username and proxy.https.username, the proxy.http value will be used.

    password

    The password with which to authenticate to the proxy. If values are provided for both proxy.http.password and proxy.https.password, the proxy.http value will be used.


    The following configuration uses an HTTPS proxy to access a Git repository.

    spring:
    +  profiles:
    +    active: git
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          proxy:
    +            https:
    +              host: my-proxy.host.io
    +              password: myproxypassword
    +              port: '3128'
    +              username: myproxyusername
    +              nonProxyHosts: example.com

    5.1.6 Sharing Configuration With All Applications

    Sharing configuration between all applications varies according to which approach you take, as described in the following topics:

    File Based Repositories

    With file-based (git, svn, and native) repositories, resources with file names in application* (application.properties, application.yml, application-*.properties, and so on) are shared between all client applications. +You can use resources with these file names to configure global defaults and have them be overridden by application-specific files as necessary.

    The #_property_overrides[property overrides] feature can also be used for setting global defaults, with placeholders applications +allowed to override them locally.

    [Tip]Tip

    With the native profile (a local file system backend) , you should use an explicit search location that is not part of the server’s own configuration. +Otherwise, the application* resources in the default search locations get removed because they are part of the server.

    Vault Server

    When using Vault as a backend, you can share configuration with all applications by placing configuration in secret/application. +For example, if you run the following Vault command, all applications using the config server will have the properties foo and baz available to them:

    $ vault write secret/application foo=bar baz=bam

    CredHub Server

    When using CredHub as a backend, you can share configuration with all applications by placing configuration in /application/ or by placing it in the default profile for the application. +For example, if you run the following CredHub command, all applications using the config server will have the properties shared.color1 and shared.color2 available to them:

    credhub set --name "/application/profile/master/shared" --type=json
    +value: {"shared.color1": "blue", "shared.color": "red"}
    credhub set --name "/my-app/default/master/more-shared" --type=json
    +value: {"shared.word1": "hello", "shared.word2": "world"}

    5.1.7 JDBC Backend

    Spring Cloud Config Server supports JDBC (relational database) as a backend for configuration properties. +You can enable this feature by adding spring-jdbc to the classpath and using the jdbc profile or by adding a bean of type JdbcEnvironmentRepository. +If you include the right dependencies on the classpath (see the user guide for more details on that), Spring Boot configures a data source.

    The database needs to have a table called PROPERTIES with columns called APPLICATION, PROFILE, and LABEL (with the usual Environment meaning), plus KEY and VALUE for the key and value pairs in Properties style. +All fields are of type String in Java, so you can make them VARCHAR of whatever length you need. +Property values behave in the same way as they would if they came from Spring Boot properties files named {application}-{profile}.properties, including all the encryption and decryption, which will be applied as post-processing steps (that is, not in the repository implementation directly).

    5.1.8 CredHub Backend

    Spring Cloud Config Server supports CredHub as a backend for configuration properties. +You can enable this feature by adding a dependency to Spring CredHub.

    pom.xml.  +

    <dependencies>
    +	<dependency>
    +		<groupId>org.springframework.credhub</groupId>
    +		<artifactId>spring-credhub-starter</artifactId>
    +	</dependency>
    +</dependencies>

    +

    The following configuration uses mutual TLS to access a CredHub:

    spring:
    +  profiles:
    +    active: credhub
    +  cloud:
    +    config:
    +      server:
    +        credhub:
    +          url: https://credhub:8844

    The properties should be stored as JSON, such as:

    credhub set --name "/demo-app/default/master/toggles" --type=json
    +value: {"toggle.button": "blue", "toggle.link": "red"}
    credhub set --name "/demo-app/default/master/abs" --type=json
    +value: {"marketing.enabled": true, "external.enabled": false}

    All client applications with the name spring.cloud.config.name=demo-app will have the following properties available to them:

    {
    +    toggle.button: "blue",
    +    toggle.link: "red",
    +    marketing.enabled: true,
    +    external.enabled: false
    +}
    [Note]Note

    When no profile is specified default will be used and when no label is specified master will be used as a default value. +NOTE: Values added to application will be shared by all the applications.

    OAuth 2.0

    You can authenticate with OAuth 2.0 using UAA as a provider.

    pom.xml.  +

    <dependencies>
    +	<dependency>
    +		<groupId>org.springframework.security</groupId>
    +		<artifactId>spring-security-config</artifactId>
    +	</dependency>
    +	<dependency>
    +		<groupId>org.springframework.security</groupId>
    +		<artifactId>spring-security-oauth2-client</artifactId>
    +	</dependency>
    +</dependencies>

    +

    The following configuration uses OAuth 2.0 and UAA to access a CredHub:

    spring:
    +  profiles:
    +    active: credhub
    +  cloud:
    +    config:
    +      server:
    +        credhub:
    +          url: https://credhub:8844
    +          oauth2:
    +            registration-id: credhub-client
    +  security:
    +    oauth2:
    +      client:
    +        registration:
    +          credhub-client:
    +            provider: uaa
    +            client-id: credhub_config_server
    +            client-secret: asecret
    +            authorization-grant-type: client_credentials
    +        provider:
    +          uaa:
    +            token-uri: https://uaa:8443/oauth/token
    [Note]Note

    The used UAA client-id should have credhub.read as scope.

    5.1.9 Composite Environment Repositories

    In some scenarios, you may wish to pull configuration data from multiple environment repositories. +To do so, you can enable the composite profile in your configuration server’s application properties or YAML file. +If, for example, you want to pull configuration data from a Subversion repository as well as two Git repositories, you can set the following properties for your configuration server:

    spring:
    +  profiles:
    +    active: composite
    +  cloud:
    +    config:
    +      server:
    +        composite:
    +        -
    +          type: svn
    +          uri: file:///path/to/svn/repo
    +        -
    +          type: git
    +          uri: file:///path/to/rex/git/repo
    +        -
    +          type: git
    +          uri: file:///path/to/walter/git/repo

    Using this configuration, precedence is determined by the order in which repositories are listed under the composite key. +In the above example, the Subversion repository is listed first, so a value found in the Subversion repository will override values found for the same property in one of the Git repositories. +A value found in the rex Git repository will be used before a value found for the same property in the walter Git repository.

    If you want to pull configuration data only from repositories that are each of distinct types, you can enable the corresponding profiles, rather than the composite profile, in your configuration server’s application properties or YAML file. +If, for example, you want to pull configuration data from a single Git repository and a single HashiCorp Vault server, you can set the following properties for your configuration server:

    spring:
    +  profiles:
    +    active: git, vault
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: file:///path/to/git/repo
    +          order: 2
    +        vault:
    +          host: 127.0.0.1
    +          port: 8200
    +          order: 1

    Using this configuration, precedence can be determined by an order property. +You can use the order property to specify the priority order for all your repositories. +The lower the numerical value of the order property, the higher priority it has. +The priority order of a repository helps resolve any potential conflicts between repositories that contain values for the same properties.

    [Note]Note

    If your composite environment includes a Vault server as in the previous example, you must include a Vault token in every request made to the configuration server. See Vault Backend.

    [Note]Note

    Any type of failure when retrieving values from an environment repository results in a failure for the entire composite environment.

    [Note]Note

    When using a composite environment, it is important that all repositories contain the same labels. +If you have an environment similar to those in the preceding examples and you request configuration data with the master label but the Subversion repository does not contain a branch called master, the entire request fails.

    Custom Composite Environment Repositories

    In addition to using one of the environment repositories from Spring Cloud, you can also provide your own EnvironmentRepository bean to be included as part of a composite environment. +To do so, your bean must implement the EnvironmentRepository interface. +If you want to control the priority of your custom EnvironmentRepository within the composite environment, you should also implement the Ordered interface and override the getOrdered method. +If you do not implement the Ordered interface, your EnvironmentRepository is given the lowest priority.

    5.1.10 Property Overrides

    The Config Server has an overrides feature that lets the operator provide configuration properties to all applications. +The overridden properties cannot be accidentally changed by the application with the normal Spring Boot hooks. +To declare overrides, add a map of name-value pairs to spring.cloud.config.server.overrides, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        overrides:
    +          foo: bar

    The preceding examples causes all applications that are config clients to read foo=bar, independent of their own configuration.

    [Note]Note

    A configuration system cannot force an application to use configuration data in any particular way. +Consequently, overrides are not enforceable. +However, they do provide useful default behavior for Spring Cloud Config clients.

    [Tip]Tip

    Normally, Spring environment placeholders with ${} can be escaped (and resolved on the client) by using backslash (\) to escape the $ or the {. +For example, \${app.foo:bar} resolves to bar, unless the app provides its own app.foo.

    [Note]Note

    In YAML, you do not need to escape the backslash itself. +However, in properties files, you do need to escape the backslash, when you configure the overrides on the server.

    You can change the priority of all overrides in the client to be more like default values, letting applications supply their own values in environment variables or System properties, by setting the spring.cloud.config.overrideNone=true flag (the default is false) in the remote repository.

    5.2 Health Indicator

    Config Server comes with a Health Indicator that checks whether the configured EnvironmentRepository is working. +By default, it asks the EnvironmentRepository for an application named app, the default profile, and the default label provided by the EnvironmentRepository implementation.

    You can configure the Health Indicator to check more applications along with custom profiles and custom labels, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        health:
    +          repositories:
    +            myservice:
    +              label: mylabel
    +            myservice-dev:
    +              name: myservice
    +              profiles: development

    You can disable the Health Indicator by setting spring.cloud.config.server.health.enabled=false.

    5.3 Security

    You can secure your Config Server in any way that makes sense to you (from physical network security to OAuth2 bearer tokens), because Spring Security and Spring Boot offer support for many security arrangements.

    To use the default Spring Boot-configured HTTP Basic security, include Spring Security on the classpath (for example, through spring-boot-starter-security). +The default is a username of user and a randomly generated password. A random password is not useful in practice, so we recommend you configure the password (by setting spring.security.user.password) and encrypt it (see below for instructions on how to do that).

    5.4 Encryption and Decryption

    [Important]Important

    To use the encryption and decryption features you need the full-strength JCE installed in your JVM (it is not included by default). +You can download the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files from Oracle and follow the installation instructions (essentially, you need to replace the two policy files in the JRE lib/security directory with the ones that you downloaded).

    If the remote property sources contain encrypted content (values starting with {cipher}), they are decrypted before sending to clients over HTTP. +The main advantage of this setup is that the property values need not be in plain text when they are at rest (for example, in a git repository). +If a value cannot be decrypted, it is removed from the property source and an additional property is added with the same key but prefixed with invalid and a value that means not applicable (usually <n/a>). +This is largely to prevent cipher text being used as a password and accidentally leaking.

    If you set up a remote config repository for config client applications, it might contain an application.yml similar to the following:

    application.yml.  +

    spring:
    +  datasource:
    +    username: dbuser
    +    password: '{cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ'

    +

    Encrypted values in a .properties file must not be wrapped in quotes. Otherwise, the value is not decrypted. The following example shows values that would work:

    application.properties.  +

    spring.datasource.username: dbuser
    +spring.datasource.password: {cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ

    +

    You can safely push this plain text to a shared git repository, and the secret password remains protected.

    The server also exposes /encrypt and /decrypt endpoints (on the assumption that these are secured and only accessed by authorized agents). +If you edit a remote config file, you can use the Config Server to encrypt values by POSTing to the /encrypt endpoint, as shown in the following example:

    $ curl localhost:8888/encrypt -d mysecret
    +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    [Note]Note

    If the value you encrypt has characters in it that need to be URL encoded, you should use the --data-urlencode option to curl to make sure they are encoded properly.

    [Tip]Tip

    Be sure not to include any of the curl command statistics in the encrypted value. +Outputting the value to a file can help avoid this problem.

    The inverse operation is also available through /decrypt (provided the server is +configured with a symmetric key or a full key pair), as shown in the following example:

    $ curl localhost:8888/decrypt -d 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    +mysecret
    [Tip]Tip

    If you testing with curl, then use --data-urlencode (instead of -d) or set an explicit Content-Type: text/plain to make sure curl encodes the data correctly when there are special characters ('+' is particularly tricky).

    Take the encrypted value and add the {cipher} prefix before you put it in the YAML or properties file and before you commit and push it to a remote (potentially insecure) store.

    The /encrypt and /decrypt endpoints also both accept paths in the form of /*/{application}/{profiles}, which can be used to control cryptography on a per-application (name) and per-profile basis when clients call into the main environment resource.

    [Note]Note

    To control the cryptography in this granular way, you must also provide a @Bean of type TextEncryptorLocator that creates a different encryptor per name and profiles. +The one that is provided by default does not do so (all encryptions use the same key).

    The spring command line client (with Spring Cloud CLI extensions +installed) can also be used to encrypt and decrypt, as shown in the following example:

    $ spring encrypt mysecret --key foo
    +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    +$ spring decrypt --key foo 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    +mysecret

    To use a key in a file (such as an RSA public key for encryption), prepend +the key value with "@" and provide the file path, as shown in the following example:

    $ spring encrypt mysecret --key @${HOME}/.ssh/id_rsa.pub
    +AQAjPgt3eFZQXwt8tsHAVv/QHiY5sI2dRcR+...
    [Note]Note

    The --key argument is mandatory (despite having a -- prefix).

    5.5 Key Management

    The Config Server can use a symmetric (shared) key or an asymmetric one (RSA key pair). +The asymmetric choice is superior in terms of security, but it is often more convenient to use a symmetric key since it is a single property value to configure in the bootstrap.properties.

    To configure a symmetric key, you need to set encrypt.key to a secret String (or use the ENCRYPT_KEY environment variable to keep it out of plain-text configuration files).

    [Note]Note

    You cannot configure an asymmetric key using encrypt.key.

    To configure an asymmetric key use a keystore (e.g. as +created by the keytool utility that comes with the JDK). The +keystore properties are encrypt.keyStore.* with * equal to

    PropertyDescription

    encrypt.keyStore.location

    Contains a Resource location

    encrypt.keyStore.password

    Holds the password that unlocks the keystore

    encrypt.keyStore.alias

    Identifies which key in the store to use

    encrypt.keyStore.type

    The type of KeyStore to create. Defaults to jks.

    The encryption is done with the public key, and a private key is +needed for decryption. +Thus, in principle, you can configure only the public key in the server if you want to only encrypt (and are prepared to decrypt the values yourself locally with the private key). +In practice, you might not want to do decrypt locally, because it spreads the key management process around all the clients, instead of +concentrating it in the server. +On the other hand, it can be a useful option if your config server is relatively insecure and only a handful of clients need the encrypted properties.

    5.6 Creating a Key Store for Testing

    To create a keystore for testing, you can use a command resembling the following:

    $ keytool -genkeypair -alias mytestkey -keyalg RSA \
    +  -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" \
    +  -keypass changeme -keystore server.jks -storepass letmein
    [Note]Note

    When using JDK 11 or above you may get the following warning when using the command above. In this case +you probably want to make sure the keypass and storepass values match.

    Warning:  Different store and key passwords not supported for PKCS12 KeyStores. Ignoring user-specified -keypass value.

    Put the server.jks file in the classpath (for instance) and then, in +your bootstrap.yml, for the Config Server, create the following settings:

    encrypt:
    +  keyStore:
    +    location: classpath:/server.jks
    +    password: letmein
    +    alias: mytestkey
    +    secret: changeme

    5.7 Using Multiple Keys and Key Rotation

    In addition to the {cipher} prefix in encrypted property values, the Config Server looks for zero or more {name:value} prefixes before the start of the (Base64 encoded) cipher text. +The keys are passed to a TextEncryptorLocator, which can do whatever logic it needs to locate a TextEncryptor for the cipher. +If you have configured a keystore (encrypt.keystore.location), the default locator looks for keys with aliases supplied by the key prefix, with a cipher text like resembling the following:

    foo:
    +  bar: `{cipher}{key:testkey}...`

    The locator looks for a key named "testkey". +A secret can also be supplied by using a {secret:…​} value in the prefix. +However, if it is not supplied, the default is to use the keystore password (which is what you get when you build a keystore and do not specify a secret). +If you do supply a secret, you should also encrypt the secret using a custom SecretLocator.

    When the keys are being used only to encrypt a few bytes of configuration data (that is, they are not being used elsewhere), key rotation is hardly ever necessary on cryptographic grounds. +However, you might occasionally need to change the keys (for example, in the event of a security breach). +In that case, all the clients would need to change their source config files (for example, in git) and use a new {key:…​} prefix in all the ciphers. +Note that the clients need to first check that the key alias is available in the Config Server keystore.

    [Tip]Tip

    If you want to let the Config Server handle all encryption as well as decryption, the {name:value} prefixes can also be added as plain text posted to the /encrypt endpoint, .

    5.8 Serving Encrypted Properties

    Sometimes you want the clients to decrypt the configuration locally, instead of doing it in the server. +In that case, if you provide the encrypt.* configuration to locate a key, you can still have /encrypt and /decrypt endpoints, but you need to explicitly switch off the decryption of outgoing properties by placing spring.cloud.config.server.encrypt.enabled=false in bootstrap.[yml|properties]. +If you do not care about the endpoints, it should work if you do not configure either the key or the enabled flag.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_consul.html b/Greenwich.SR5/multi/multi__spring_cloud_consul.html new file mode 100644 index 00000000..0d2dbd43 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_consul.html @@ -0,0 +1,9 @@ + + + Part IX. Spring Cloud Consul

    Part IX. Spring Cloud Consul

    Greenwich.SR5

    This project provides Consul integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms. With a few +simple annotations you can quickly enable and configure the common patterns inside your +application and build large distributed systems with Consul based components. The +patterns provided include Service Discovery, Control Bus and Configuration. +Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon), Circuit Breaker +(Hystrix) are provided by integration with Spring Cloud Netflix.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_context_application_context_services.html b/Greenwich.SR5/multi/multi__spring_cloud_context_application_context_services.html new file mode 100644 index 00000000..101cd8ca --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_context_application_context_services.html @@ -0,0 +1,98 @@ + + + 2. Spring Cloud Context: Application Context Services

    2. Spring Cloud Context: Application Context Services

    Spring Boot has an opinionated view of how to build an application with Spring. +For instance, it has conventional locations for common configuration files and has endpoints for common management and monitoring tasks. +Spring Cloud builds on top of that and adds a few features that probably all components in a system would use or occasionally need.

    2.1 The Bootstrap Application Context

    A Spring Cloud application operates by creating a bootstrap context, which is a parent context for the main application. +It is responsible for loading configuration properties from the external sources and for decrypting properties in the local external configuration files. +The two contexts share an Environment, which is the source of external properties for any Spring application. +By default, bootstrap properties (not bootstrap.properties but properties that are loaded during the bootstrap phase) are added with high precedence, so they cannot be overridden by local configuration.

    The bootstrap context uses a different convention for locating external configuration than the main application context. +Instead of application.yml (or .properties), you can use bootstrap.yml, keeping the external configuration for bootstrap and main context +nicely separate. +The following listing shows an example:

    bootstrap.yml.  +

    spring:
    +  application:
    +    name: foo
    +  cloud:
    +    config:
    +      uri: ${SPRING_CONFIG_URI:http://localhost:8888}

    +

    If your application needs any application-specific configuration from the server, it is a good idea to set the spring.application.name (in bootstrap.yml or application.yml). +In order for the property spring.application.name to be used as the application’s context ID you +must set it in bootstrap.[properties | yml].

    If you want to retrieve specific profile configuration, you should also set spring.profiles.active in bootstrap.[properties | yml].

    You can disable the bootstrap process completely by setting spring.cloud.bootstrap.enabled=false (for example, in system properties).

    2.2 Application Context Hierarchies

    If you build an application context from SpringApplication or SpringApplicationBuilder, then the Bootstrap context is added as a parent to that context. +It is a feature of Spring that child contexts inherit property sources and profiles from their parent, so the main application context contains additional property sources, compared to building the same context without Spring Cloud Config. +The additional property sources are:

    • bootstrap: If any PropertySourceLocators are found in the Bootstrap context and if they have non-empty properties, an optional CompositePropertySource appears with high priority. +An example would be properties from the Spring Cloud Config Server. +See Section 2.6, “Customizing the Bootstrap Property Sources” for instructions on how to customize the contents of this property source.
    • applicationConfig: [classpath:bootstrap.yml] (and related files if Spring profiles are active): If you have a bootstrap.yml (or .properties), those properties are used to configure the Bootstrap context. +Then they get added to the child context when its parent is set. +They have lower precedence than the application.yml (or .properties) and any other property sources that are added to the child as a normal part of the process of creating a Spring Boot application. +See Section 2.3, “Changing the Location of Bootstrap Properties” for instructions on how to customize the contents of these property sources.

    Because of the ordering rules of property sources, the bootstrap entries take precedence. +However, note that these do not contain any data from bootstrap.yml, which has very low precedence but can be used to set defaults.

    You can extend the context hierarchy by setting the parent context of any ApplicationContext you create — for example, by using its own interface or with the SpringApplicationBuilder convenience methods (parent(), child() and sibling()). +The bootstrap context is the parent of the most senior ancestor that you create yourself. +Every context in the hierarchy has its own bootstrap (possibly empty) property source to avoid promoting values inadvertently from parents down to their descendants. +If there is a Config Server, every context in the hierarchy can also (in principle) have a different spring.application.name and, hence, a different remote property source. +Normal Spring application context behavior rules apply to property resolution: properties from a child context override those in +the parent, by name and also by property source name. +(If the child has a property source with the same name as the parent, the value from the parent is not included in the child).

    Note that the SpringApplicationBuilder lets you share an Environment amongst the whole hierarchy, but that is not the default. +Thus, sibling contexts, in particular, do not need to have the same profiles or property sources, even though they may share common values with their parent.

    2.3 Changing the Location of Bootstrap Properties

    The bootstrap.yml (or .properties) location can be specified by setting spring.cloud.bootstrap.name (default: bootstrap), spring.cloud.bootstrap.location (default: empty) or spring.cloud.bootstrap.additional-location (default: empty) — for example, in System properties. +Those properties behave like the spring.config.* variants with the same name. +With spring.cloud.bootstrap.location the default locations are replaced and only the specified ones are used. +To add locations to the list of default ones, spring.cloud.bootstrap.additional-location could be used. +In fact, they are used to set up the bootstrap ApplicationContext by setting those properties in its Environment. +If there is an active profile (from spring.profiles.active or through the Environment API in the +context you are building), properties in that profile get loaded as well, the same as in a regular Spring Boot app — for example, from bootstrap-development.properties for a development profile.

    2.4 Overriding the Values of Remote Properties

    The property sources that are added to your application by the bootstrap context are often remote (from example, from Spring Cloud Config Server). +By default, they cannot be overridden locally. +If you want to let your applications override the remote properties with their own System properties or config files, the remote property source has to grant it permission by setting spring.cloud.config.allowOverride=true (it does not work to set this locally). +Once that flag is set, two finer-grained settings control the location of the remote properties in relation to system properties and the application’s local configuration:

    • spring.cloud.config.overrideNone=true: Override from any local property source.
    • spring.cloud.config.overrideSystemProperties=false: Only system properties, command line arguments, and environment variables (but not the local config files) should override the remote settings.

    2.5 Customizing the Bootstrap Configuration

    The bootstrap context can be set to do anything you like by adding entries to /META-INF/spring.factories under a key named org.springframework.cloud.bootstrap.BootstrapConfiguration. +This holds a comma-separated list of Spring @Configuration classes that are used to create the context. +Any beans that you want to be available to the main application context for autowiring can be created here. +There is a special contract for @Beans of type ApplicationContextInitializer. +If you want to control the startup sequence, classes can be marked with an @Order annotation (the default order is last).

    [Warning]Warning

    When adding custom BootstrapConfiguration, be careful that the classes you add are not @ComponentScanned by mistake into your main application context, where they might not be needed. +Use a separate package name for boot configuration classes and make sure that name is not already covered by your @ComponentScan or @SpringBootApplication annotated configuration classes.

    The bootstrap process ends by injecting initializers into the main SpringApplication instance (which is the normal Spring Boot startup sequence, whether it is running as a standalone application or deployed in an application server). +First, a bootstrap context is created from the classes found in spring.factories. +Then, all @Beans of type ApplicationContextInitializer are added to the main SpringApplication before it is started.

    2.6 Customizing the Bootstrap Property Sources

    The default property source for external configuration added by the bootstrap process is the Spring Cloud Config Server, but you can add additional sources by adding beans of type PropertySourceLocator to the bootstrap context (through spring.factories). +For instance, you can insert additional properties from a different server or from a database.

    As an example, consider the following custom locator:

    @Configuration
    +public class CustomPropertySourceLocator implements PropertySourceLocator {
    +
    +    @Override
    +    public PropertySource<?> locate(Environment environment) {
    +        return new MapPropertySource("customProperty",
    +                Collections.<String, Object>singletonMap("property.from.sample.custom.source", "worked as intended"));
    +    }
    +
    +}

    The Environment that is passed in is the one for the ApplicationContext about to be created — in other words, the one for which we supply additional property sources for. +It already has its normal Spring Boot-provided property sources, so you can use those to locate a property source specific to this Environment (for example, by keying it on spring.application.name, as is done in the default Spring Cloud Config Server property source locator).

    If you create a jar with this class in it and then add a META-INF/spring.factories containing the following, the customProperty PropertySource appears in any application that includes that jar on its classpath:

    org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomPropertySourceLocator

    2.7 Logging Configuration

    If you are going to use Spring Boot to configure log settings than +you should place this configuration in `bootstrap.[yml | properties] +if you would like it to apply to all events.

    [Note]Note

    For Spring Cloud to initialize logging configuration properly you cannot use a custom prefix. For example, +using custom.loggin.logpath will not be recognized by Spring Cloud when initializing the logging system.

    2.8 Environment Changes

    The application listens for an EnvironmentChangeEvent and reacts to the change in a couple of standard ways (additional ApplicationListeners can be added as @Beans by the user in the normal way). +When an EnvironmentChangeEvent is observed, it has a list of key values that have changed, and the application uses those to:

    • Re-bind any @ConfigurationProperties beans in the context
    • Set the logger levels for any properties in logging.level.*

    Note that the Config Client does not, by default, poll for changes in the Environment. +Generally, we would not recommend that approach for detecting changes (although you could set it up with a +@Scheduled annotation). +If you have a scaled-out client application, it is better to broadcast the EnvironmentChangeEvent to all the instances instead of having them polling for changes (for example, by using the Spring Cloud Bus).

    The EnvironmentChangeEvent covers a large class of refresh use cases, as long as you can actually make a change to the Environment and publish the event. +Note that those APIs are public and part of core Spring). +You can verify that the changes are bound to @ConfigurationProperties beans by visiting the /configprops endpoint (a normal Spring Boot Actuator feature). +For instance, a DataSource can have its maxPoolSize changed at runtime (the default DataSource created by Spring Boot is an @ConfigurationProperties bean) and grow capacity dynamically. +Re-binding @ConfigurationProperties does not cover another large class of use cases, where you need more control over the refresh and where you need a change to be atomic over the whole ApplicationContext. +To address those concerns, we have @RefreshScope.

    2.9 Refresh Scope

    When there is a configuration change, a Spring @Bean that is marked as @RefreshScope gets special treatment. +This feature addresses the problem of stateful beans that only get their configuration injected when they are initialized. +For instance, if a DataSource has open connections when the database URL is changed via the Environment, you probably want the holders of those connections to be able to complete what they are doing. +Then, the next time something borrows a connection from the pool, it gets one with the new URL.

    Sometimes, it might even be mandatory to apply the @RefreshScope +annotation on some beans which can be only initialized once. If a bean +is "immutable", you will have to either annotate the bean with @RefreshScope +or specify the classname under the property key +spring.cloud.refresh.extra-refreshable.

    [Important]Important

    If you create a DataSource bean yourself and the implementation is a HikariDataSource, return the +most specific type, in this case HikariDataSource. Otherwise, you will need to set +spring.cloud.refresh.extra-refreshable=javax.sql.DataSource.

    Refresh scope beans are lazy proxies that initialize when they are used (that is, when a method is called), and the scope acts as a cache of initialized values. +To force a bean to re-initialize on the next method call, you must invalidate its cache entry.

    The RefreshScope is a bean in the context and has a public refreshAll() method to refresh all beans in the scope by clearing the target cache. +The /refresh endpoint exposes this functionality (over HTTP or JMX). +To refresh an individual bean by name, there is also a refresh(String) method.

    To expose the /refresh endpoint, you need to add following configuration to your application:

    management:
    +  endpoints:
    +    web:
    +      exposure:
    +        include: refresh
    [Note]Note

    @RefreshScope works (technically) on an @Configuration class, but it might lead to surprising behavior. +For example, it does not mean that all the @Beans defined in that class are themselves in @RefreshScope. +Specifically, anything that depends on those beans cannot rely on them being updated when a refresh is initiated, unless it is itself in @RefreshScope. +In that case, it is rebuilt on a refresh and its dependencies are re-injected. At that point, they are re-initialized from the refreshed @Configuration).

    2.10 Encryption and Decryption

    Spring Cloud has an Environment pre-processor for decrypting property values locally. +It follows the same rules as the Config Server and has the same external configuration through encrypt.*. +Thus, you can use encrypted values in the form of {cipher}* and, as long as there is a valid key, they are decrypted before the main application context gets the Environment settings. +To use the encryption features in an application, you need to include Spring Security RSA in your classpath (Maven co-ordinates: "org.springframework.security:spring-security-rsa"), and you also need the full strength JCE extensions in your JVM.

    If you get an exception due to "Illegal key size" and you use Sun’s JDK, you need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. +See the following links for more information:

    Extract the files into the JDK/jre/lib/security folder for whichever version of JRE/JDK x64/x86 you use.

    2.11 Endpoints

    For a Spring Boot Actuator application, some additional management endpoints are available. You can use:

    • POST to /actuator/env to update the Environment and rebind @ConfigurationProperties and log levels.
    • /actuator/refresh to re-load the boot strap context and refresh the @RefreshScope beans.
    • /actuator/restart to close the ApplicationContext and restart it (disabled by default).
    • /actuator/pause and /actuator/resume for calling the Lifecycle methods (stop() and start() on the ApplicationContext).
    [Note]Note

    If you disable the /actuator/restart endpoint then the /actuator/pause and /actuator/resume endpoints +will also be disabled since they are just a special case of /actuator/restart.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract.html b/Greenwich.SR5/multi/multi__spring_cloud_contract.html new file mode 100644 index 00000000..0b93df63 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract.html @@ -0,0 +1,4 @@ + + + Part XIII. Spring Cloud Contract

    Part XIII. Spring Cloud Contract

    Documentation Authors: Adam Dudczak, Mathias Düsterhöft, Marcin Grzejszczak, Dennis Kieselhorst, Jakub Kubryński, Karol Lassak, +Olga Maciaszek-Sharma, Mariusz Smykuła, Dave Syer, Jay Bryant

    Greenwich.SR5

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract_2.html b/Greenwich.SR5/multi/multi__spring_cloud_contract_2.html new file mode 100644 index 00000000..499201fd --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract_2.html @@ -0,0 +1,7 @@ + + + 86. Spring Cloud Contract

    86. Spring Cloud Contract

    You need confidence when pushing new features to a new application or service in a +distributed system. This project provides support for Consumer Driven Contracts and +service schemas in Spring applications (for both HTTP and message-based interactions), +covering a range of options for writing tests, publishing them as assets, and asserting +that a contract is kept by producers and consumers.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract_faq.html b/Greenwich.SR5/multi/multi__spring_cloud_contract_faq.html new file mode 100644 index 00000000..7bf456bc --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract_faq.html @@ -0,0 +1,702 @@ + + + 88. Spring Cloud Contract FAQ

    88. Spring Cloud Contract FAQ

    88.1 Why use Spring Cloud Contract Verifier and not X ?

    For the time being Spring Cloud Contract is a JVM based tool. So it could be your first pick when you’re already creating +software for the JVM. This project has a lot of really interesting features but especially quite a few of them definitely make +Spring Cloud Contract Verifier stand out on the "market" of Consumer Driven Contract (CDC) tooling. Out of many the most interesting are:

    • Possibility to do CDC with messaging
    • Clear and easy to use, statically typed DSL
    • Possibility to copy paste your current JSON file to the contract and only edit its elements
    • Automatic generation of tests from the defined Contract
    • Stub Runner functionality - the stubs are automatically downloaded at runtime from Nexus / Artifactory
    • Spring Cloud integration - no discovery service is needed for integration tests
    • Spring Cloud Contract integrates with Pact out of the box and provides easy hooks to extend its functionality
    • Via Docker adds support for any language & framework used

    88.2 I don’t want to write a contract in Groovy!

    No problem. You can write a contract in YAML!

    88.3 What is this value(consumer(), producer()) ?

    One of the biggest challenges related to stubs is their reusability. Only if they can be vastly used, will they serve their purpose. +What typically makes that difficult are the hard-coded values of request / response elements. For example dates or ids. +Imagine the following JSON request

    {
    +    "time" : "2016-10-10 20:10:15",
    +    "id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
    +    "body" : "foo"
    +}

    and JSON response

    {
    +    "time" : "2016-10-10 21:10:15",
    +    "id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
    +    "body" : "bar"
    +}

    Imagine the pain required to set proper value of the time field (let’s assume that this content is generated by the +database) by changing the clock in the system or providing stub implementations of data providers. The same is related +to the field called id. Will you create a stubbed implementation of UUID generator? Makes little sense…​

    So as a consumer you would like to send a request that matches any form of a time or any UUID. That way your system +will work as usual - will generate data and you won’t have to stub anything out. Let’s assume that in case of the aforementioned +JSON the most important part is the body field. You can focus on that and provide matching for other fields. In other words +you would like the stub to work like this:

    {
    +    "time" : "SOMETHING THAT MATCHES TIME",
    +    "id" : "SOMETHING THAT MATCHES UUID",
    +    "body" : "foo"
    +}

    As far as the response goes as a consumer you need a concrete value that you can operate on. So such a JSON is valid

    {
    +    "time" : "2016-10-10 21:10:15",
    +    "id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
    +    "body" : "bar"
    +}

    As you could see in the previous sections we generate tests from contracts. So from the producer’s side the situation looks +much different. We’re parsing the provided contract and in the test we want to send a real request to your endpoints. +So for the case of a producer for the request we can’t have any sort of matching. We need concrete values that the +producer’s backend can work on. Such a JSON would be a valid one:

    {
    +    "time" : "2016-10-10 20:10:15",
    +    "id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
    +    "body" : "foo"
    +}

    On the other hand from the point of view of the validity of the contract the response doesn’t necessarily have to +contain concrete values of time or id. Let’s say that you generate those on the producer side - again, you’d +have to do a lot of stubbing to ensure that you always return the same values. That’s why from the producer’s side +what you might want is the following response:

    {
    +    "time" : "SOMETHING THAT MATCHES TIME",
    +    "id" : "SOMETHING THAT MATCHES UUID",
    +    "body" : "bar"
    +}

    How can you then provide one time a matcher for the consumer and a concrete value for the producer and vice versa? +In Spring Cloud Contract we’re allowing you to provide a dynamic value. That means that it can differ for both +sides of the communication. You can pass the values:

    Either via the value method

    value(consumer(...), producer(...))
    +value(stub(...), test(...))
    +value(client(...), server(...))

    or using the $() method

    $(consumer(...), producer(...))
    +$(stub(...), test(...))
    +$(client(...), server(...))

    You can read more about this in the Chapter 94, Contract DSL section.

    Calling value() or $() tells Spring Cloud Contract that you will be passing a dynamic value. +Inside the consumer() method you pass the value that should be used on the consumer side (in the generated stub). +Inside the producer() method you pass the value that should be used on the producer side (in the generated test).

    [Tip]Tip

    If on one side you have passed the regular expression and you haven’t passed the other, then the +other side will get auto-generated.

    Most often you will use that method together with the regex helper method. E.g. consumer(regex('[0-9]{10}')).

    To sum it up the contract for the aforementioned scenario would look more or less like this (the regular expression +for time and UUID are simplified and most likely invalid but we want to keep things very simple in this example):

    org.springframework.cloud.contract.spec.Contract.make {
    +				request {
    +					method 'GET'
    +					url '/someUrl'
    +					body([
    +					    time : value(consumer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
    +					    id: value(consumer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
    +					    body: "foo"
    +					])
    +				}
    +			response {
    +				status OK()
    +				body([
    +					    time : value(producer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
    +					    id: value([producer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
    +					    body: "bar"
    +					])
    +			}
    +}
    [Important]Important

    Please read the Groovy docs related to JSON to understand how to +properly structure the request / response bodies.

    88.4 How to do Stubs versioning?

    88.4.1 API Versioning

    Let’s try to answer a question what versioning really means. If you’re referring to the API version then there are +different approaches.

    • use Hypermedia, links and do not version your API by any means
    • pass versions through headers / urls

    I will not try to answer a question which approach is better. Whatever suits your needs and allows you to generate +business value should be picked.

    Let’s assume that you do version your API. In that case you should provide as many contracts as many versions you support. +You can create a subfolder for every version or append it to the contract name - whatever suits you more.

    88.4.2 JAR versioning

    If by versioning you mean the version of the JAR that contains the stubs then there are essentially two main approaches.

    Let’s assume that you’re doing Continuous Delivery / Deployment which means that you’re generating a new version of +the jar each time you go through the pipeline and that jar can go to production at any time. For example your jar version +looks like this (it got built on the 20.10.2016 at 20:15:21) :

    1.0.0.20161020-201521-RELEASE

    In that case your generated stub jar will look like this.

    1.0.0.20161020-201521-RELEASE-stubs.jar

    In this case you should inside your application.yml or @AutoConfigureStubRunner when referencing stubs provide the + latest version of the stubs. You can do that by passing the + sign. Example

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})

    If the versioning however is fixed (e.g. 1.0.4.RELEASE or 2.1.1) then you have to set the concrete value of the jar +version. Example for 2.1.1.

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:2.1.1:stubs:8080"})

    88.4.3 Dev or prod stubs

    You can manipulate the classifier to run the tests against current development version of the stubs of other services + or the ones that were deployed to production. If you alter your build to deploy the stubs with the prod-stubs classifier + once you reach production deployment then you can run tests in one case with dev stubs and one with prod stubs.

    Example of tests using development version of stubs

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})

    Example of tests using production version of stubs

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:prod-stubs:8080"})

    You can pass those values also via properties from your deployment pipeline.

    88.5 Common repo with contracts

    Another way of storing contracts other than having them with the producer is keeping them in a common place. +It can be related to security issues where the consumers can’t clone the producer’s code. Also if you keep +contracts in a single place then you, as a producer, will know how many consumers you have and which +consumer you will break with your local changes.

    88.5.1 Repo structure

    Let’s assume that we have a producer with coordinates com.example:server and 3 consumers: client1, +client2, client3. Then in the repository with common contracts you would have the following setup +(which you can checkout here):

    ├── com
    +│   └── example
    +│       └── server
    +│           ├── client1
    +│           │   └── expectation.groovy
    +│           ├── client2
    +│           │   └── expectation.groovy
    +│           ├── client3
    +│           │   └── expectation.groovy
    +│           └── pom.xml
    +├── mvnw
    +├── mvnw.cmd
    +├── pom.xml
    +└── src
    +    └── assembly
    +        └── contracts.xml

    As you can see under the slash-delimited groupid / artifact id folder (com/example/server) you have +expectations of the 3 consumers (client1, client2 and client3). Expectations are the standard Groovy DSL +contract files as described throughout this documentation. This repository has to produce a JAR file that maps +one to one to the contents of the repo.

    Example of a pom.xml inside the server folder.

    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +		 xmlns="http://maven.apache.org/POM/4.0.0"
    +		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    +	<modelVersion>4.0.0</modelVersion>
    +
    +	<groupId>com.example</groupId>
    +	<artifactId>server</artifactId>
    +	<version>0.0.1-SNAPSHOT</version>
    +
    +	<name>Server Stubs</name>
    +	<description>POM used to install locally stubs for consumer side</description>
    +
    +	<parent>
    +		<groupId>org.springframework.boot</groupId>
    +		<artifactId>spring-boot-starter-parent</artifactId>
    +		<version>2.1.10.RELEASE</version>
    +		<relativePath/>
    +	</parent>
    +
    +	<properties>
    +		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +		<java.version>1.8</java.version>
    +		<spring-cloud-contract.version>2.1.6.BUILD-SNAPSHOT</spring-cloud-contract.version>
    +		<spring-cloud-release.version>Greenwich.BUILD-SNAPSHOT
    +		</spring-cloud-release.version>
    +		<excludeBuildFolders>true</excludeBuildFolders>
    +	</properties>
    +
    +	<dependencyManagement>
    +		<dependencies>
    +			<dependency>
    +				<groupId>org.springframework.cloud</groupId>
    +				<artifactId>spring-cloud-dependencies</artifactId>
    +				<version>${spring-cloud-release.version}</version>
    +				<type>pom</type>
    +				<scope>import</scope>
    +			</dependency>
    +		</dependencies>
    +	</dependencyManagement>
    +
    +	<build>
    +		<plugins>
    +			<plugin>
    +				<groupId>org.springframework.cloud</groupId>
    +				<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +				<version>${spring-cloud-contract.version}</version>
    +				<extensions>true</extensions>
    +				<configuration>
    +					<!-- By default it would search under src/test/resources/ -->
    +					<contractsDirectory>${project.basedir}</contractsDirectory>
    +				</configuration>
    +			</plugin>
    +		</plugins>
    +	</build>
    +
    +	<repositories>
    +		<repository>
    +			<id>spring-snapshots</id>
    +			<name>Spring Snapshots</name>
    +			<url>https://repo.spring.io/snapshot</url>
    +			<snapshots>
    +				<enabled>true</enabled>
    +			</snapshots>
    +		</repository>
    +		<repository>
    +			<id>spring-milestones</id>
    +			<name>Spring Milestones</name>
    +			<url>https://repo.spring.io/milestone</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</repository>
    +		<repository>
    +			<id>spring-releases</id>
    +			<name>Spring Releases</name>
    +			<url>https://repo.spring.io/release</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</repository>
    +	</repositories>
    +	<pluginRepositories>
    +		<pluginRepository>
    +			<id>spring-snapshots</id>
    +			<name>Spring Snapshots</name>
    +			<url>https://repo.spring.io/snapshot</url>
    +			<snapshots>
    +				<enabled>true</enabled>
    +			</snapshots>
    +		</pluginRepository>
    +		<pluginRepository>
    +			<id>spring-milestones</id>
    +			<name>Spring Milestones</name>
    +			<url>https://repo.spring.io/milestone</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</pluginRepository>
    +		<pluginRepository>
    +			<id>spring-releases</id>
    +			<name>Spring Releases</name>
    +			<url>https://repo.spring.io/release</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</pluginRepository>
    +	</pluginRepositories>
    +
    +</project>

    As you can see there are no dependencies other than the Spring Cloud Contract Maven Plugin. +Those poms are necessary for the consumer side to run mvn clean install -DskipTests to locally install + stubs of the producer project.

    The pom.xml in the root folder can look like this:

    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +		 xmlns="http://maven.apache.org/POM/4.0.0"
    +		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    +	<modelVersion>4.0.0</modelVersion>
    +
    +	<groupId>com.example.standalone</groupId>
    +	<artifactId>contracts</artifactId>
    +	<version>0.0.1-SNAPSHOT</version>
    +
    +	<name>Contracts</name>
    +	<description>Contains all the Spring Cloud Contracts, well, contracts. JAR used by the
    +		producers to generate tests and stubs
    +	</description>
    +
    +	<properties>
    +		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +	</properties>
    +
    +	<build>
    +		<plugins>
    +			<plugin>
    +				<groupId>org.apache.maven.plugins</groupId>
    +				<artifactId>maven-assembly-plugin</artifactId>
    +				<executions>
    +					<execution>
    +						<id>contracts</id>
    +						<phase>prepare-package</phase>
    +						<goals>
    +							<goal>single</goal>
    +						</goals>
    +						<configuration>
    +							<attach>true</attach>
    +							<descriptor>${basedir}/src/assembly/contracts.xml</descriptor>
    +							<!-- If you want an explicit classifier remove the following line -->
    +							<appendAssemblyId>false</appendAssemblyId>
    +						</configuration>
    +					</execution>
    +				</executions>
    +			</plugin>
    +		</plugins>
    +	</build>
    +
    +</project>

    It’s using the assembly plugin in order to build the JAR with all the contracts. Example of such setup is here:

    <assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +		  xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    +		  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
    +	<id>project</id>
    +	<formats>
    +		<format>jar</format>
    +	</formats>
    +	<includeBaseDirectory>false</includeBaseDirectory>
    +	<fileSets>
    +		<fileSet>
    +			<directory>${project.basedir}</directory>
    +			<outputDirectory>/</outputDirectory>
    +			<useDefaultExcludes>true</useDefaultExcludes>
    +			<excludes>
    +				<exclude>**/${project.build.directory}/**</exclude>
    +				<exclude>mvnw</exclude>
    +				<exclude>mvnw.cmd</exclude>
    +				<exclude>.mvn/**</exclude>
    +				<exclude>src/**</exclude>
    +			</excludes>
    +		</fileSet>
    +	</fileSets>
    +</assembly>

    88.5.2 Workflow

    The workflow would look similar to the one presented in the Step by step guide to CDC. The only difference + is that the producer doesn’t own the contracts anymore. So the consumer and the producer have to work on + common contracts in a common repository.

    88.5.3 Consumer

    When the consumer wants to work on the contracts offline, instead of cloning the producer code, the +consumer team clones the common repository, goes to the required producer’s folder (e.g. com/example/server) +and runs mvn clean install -DskipTests to install locally the stubs converted from the contracts.

    [Tip]Tip

    You need to have Maven installed locally

    88.5.4 Producer

    As a producer it’s enough to alter the Spring Cloud Contract Verifier to provide the URL and the dependency +of the JAR containing the contracts:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<configuration>
    +		<contractsMode>REMOTE</contractsMode>
    +		<contractsRepositoryUrl>
    +			https://link/to/your/nexus/or/artifactory/or/sth
    +		</contractsRepositoryUrl>
    +		<contractDependency>
    +			<groupId>com.example.standalone</groupId>
    +			<artifactId>contracts</artifactId>
    +		</contractDependency>
    +	</configuration>
    +</plugin>

    With this setup the JAR with groupid com.example.standalone and artifactid contracts will be downloaded +from http://link/to/your/nexus/or/artifactory/or/sth. It will be then unpacked in a local temporary folder +and contracts present under the com/example/server will be picked as the ones used to generate the +tests and the stubs. Due to this convention the producer team will know which consumer teams will be broken +when some incompatible changes are done.

    The rest of the flow looks the same.

    88.5.5 How can I define messaging contracts per topic not per producer?

    To avoid messaging contracts duplication in the common repo, when few producers writing messages to one topic, +we could create the structure when the rest contracts would be placed in a folder per producer and messaging +contracts in the folder per topic.

    For Maven Project

    To make it possible to work on the producer side we should specify an inclusion pattern for +filtering common repository jar by messaging topics we are interested in. includedFiles property of Maven Spring Cloud Contract plugin +allows us to do that. Also contractsPath need to be specified since the default path would be the common repository groupid/artifactid.

    <plugin>
    +   <groupId>org.springframework.cloud</groupId>
    +   <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +   <version>${spring-cloud-contract.version}</version>
    +   <configuration>
    +      <contractsMode>REMOTE</contractsMode>
    +      <contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
    +      <contractDependency>
    +         <groupId>com.example</groupId>
    +         <artifactId>common-repo-with-contracts</artifactId>
    +         <version>+</version>
    +      </contractDependency>
    +      <contractsPath>/</contractsPath>
    +      <baseClassMappings>
    +         <baseClassMapping>
    +            <contractPackageRegex>.*messaging.*</contractPackageRegex>
    +            <baseClassFQN>com.example.services.MessagingBase</baseClassFQN>
    +         </baseClassMapping>
    +         <baseClassMapping>
    +            <contractPackageRegex>.*rest.*</contractPackageRegex>
    +            <baseClassFQN>com.example.services.TestBase</baseClassFQN>
    +         </baseClassMapping>
    +      </baseClassMappings>
    +      <includedFiles>
    +         <includedFile>**/${project.artifactId}/**</includedFile>
    +         <includedFile>**/${first-topic}/**</includedFile>
    +         <includedFile>**/${second-topic}/**</includedFile>
    +      </includedFiles>
    +   </configuration>
    +</plugin>

    For Gradle Project

    • Add a custom configuration for the common-repo dependency:
    ext {
    +    conractsGroupId = "com.example"
    +    contractsArtifactId = "common-repo"
    +    contractsVersion = "1.2.3"
    +}
    +
    +configurations {
    +    contracts {
    +        transitive = false
    +    }
    +}
    • Add the common-repo dependency to your classpath:
    dependencies {
    +    contracts "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    +    testCompile "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    +}
    • Download the dependency to an appropriate folder:
    task getContracts(type: Copy) {
    +    from configurations.contracts
    +    into new File(project.buildDir, "downloadedContracts")
    +}
    • Unzip JAR:
    task unzipContracts(type: Copy) {
    +    def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar")
    +    def outputDir = file("${buildDir}/unpackedContracts")
    +
    +    from zipTree(zipFile)
    +    into outputDir
    +}
    • Cleanup unused contracts:
    task deleteUnwantedContracts(type: Delete) {
    +    delete fileTree(dir: "${buildDir}/unpackedContracts",
    +        include: "**/*",
    +        excludes: [
    +            "**/${project.name}/**"",
    +            "**/${first-topic}/**",
    +            "**/${second-topic}/**"])
    +}
    • Create task dependencies:
    unzipContracts.dependsOn("getContracts")
    +deleteUnwantedContracts.dependsOn("unzipContracts")
    +build.dependsOn("deleteUnwantedContracts")
    • Configure plugin by specifying the directory containing contracts using contractsDslDir property
    contracts {
    +    contractsDslDir = new File("${buildDir}/unpackedContracts")
    +}

    88.6 Do I need a Binary Storage? Can’t I use Git?

    In the polyglot world, there are languages that don’t use binary storages like +Artifactory or Nexus. Starting from Spring Cloud Contract version 2.0.0 we provide +mechanisms to store contracts and stubs in a SCM repository. Currently the +only supported SCM is Git.

    The repository would have to the following setup +(which you can checkout here):

    .
    +└── META-INF
    +    └── com.example
    +        └── beer-api-producer-git
    +            └── 0.0.1-SNAPSHOT
    +                ├── contracts
    +                │   └── beer-api-consumer
    +                │       ├── messaging
    +                │       │   ├── shouldSendAcceptedVerification.groovy
    +                │       │   └── shouldSendRejectedVerification.groovy
    +                │       └── rest
    +                │           ├── shouldGrantABeerIfOldEnough.groovy
    +                │           └── shouldRejectABeerIfTooYoung.groovy
    +                └── mappings
    +                    └── beer-api-consumer
    +                        └── rest
    +                            ├── shouldGrantABeerIfOldEnough.json
    +                            └── shouldRejectABeerIfTooYoung.json

    Under META-INF folder:

    • we group applications via groupId (e.g. com.example)
    • then each application is represented via the artifactId (e.g. beer-api-producer-git)
    • next, the version of the application (e.g. 0.0.1-SNAPSHOT). Starting from Spring Cloud Contract version 2.1.0, you can specify the versions as follows (assuming that your versions follow the semantic versioning)

      • + or latest - to find the latest version of your stubs (assuming that the snapshots are always the latest artifact for a given revision number). That means:

        • if you have a version 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT and 2.0.0.RELEASE we will assume that the latest is 2.0.0.BUILD-SNAPSHOT
        • if you have a version 1.0.0.RELEASE and 2.0.0.RELEASE we will assume that the latest is 2.0.0.RELEASE
        • if you have a version called latest or + we will pick that folder
      • release - to find the latest release version of your stubs. That means:

        • if you have a version 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT and 2.0.0.RELEASE we will assume that the latest is 2.0.0.RELEASE
        • if you have a version called release we will pick that folder
    • finally, there are two folders:

      • contracts - the good practice is to store the contracts required by each +consumer in the folder with the consumer name (e.g. beer-api-consumer). That way you +can use the stubs-per-consumer feature. Further directory structure is arbitrary.
      • mappings - in this folder the Maven / Gradle Spring Cloud Contract plugins will push +the stub server mappings. On the consumer side, Stub Runner will scan this folder +to start stub servers with stub definitions. The folder structure will be a copy +of the one created in the contracts subfolder.

    88.6.1 Protocol convention

    In order to control the type and location of the source of contracts (whether it’s +a binary storage or an SCM repository), you can use the protocol in the URL of +the repository. Spring Cloud Contract iterates over registered protocol resolvers +and tries to fetch the contracts (via a plugin) or stubs (via Stub Runner).

    For the SCM functionality, currently, we support the Git repository. To use it, +in the property, where the repository URL needs to be placed you just have to prefix +the connection URL with git://. Here you can find a couple of examples:

    git://file:///foo/bar
    +git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git
    +git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git

    88.6.2 Producer

    For the producer, to use the SCM approach, we can reuse the +same mechanism we use for external contracts. We route Spring Cloud Contract +to use the SCM implementation via the URL that contains +the git:// protocol.

    [Important]Important

    You have to manually add the pushStubsToScm +goal in Maven or execute (bind) the pushStubsToScm task in +Gradle. We don’t push stubs to origin of your git +repository out of the box.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <!-- Base class mappings etc. -->
    +
    +        <!-- We want to pick contracts from a Git repository -->
    +        <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl>
    +
    +        <!-- We reuse the contract dependency section to set up the path
    +        to the folder that contains the contract definitions. In our case the
    +        path will be /groupId/artifactId/version/contracts -->
    +        <contractDependency>
    +            <groupId>${project.groupId}</groupId>
    +            <artifactId>${project.artifactId}</artifactId>
    +            <version>${project.version}</version>
    +        </contractDependency>
    +
    +        <!-- The contracts mode can't be classpath -->
    +        <contractsMode>REMOTE</contractsMode>
    +    </configuration>
    +    <executions>
    +        <execution>
    +            <phase>package</phase>
    +            <goals>
    +                <!-- By default we will not push the stubs back to SCM,
    +                you have to explicitly add it as a goal -->
    +                <goal>pushStubsToScm</goal>
    +            </goals>
    +        </execution>
    +    </executions>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +	// We want to pick contracts from a Git repository
    +	contractDependency {
    +		stringNotation = "${project.group}:${project.name}:${project.version}"
    +	}
    +	/*
    +	We reuse the contract dependency section to set up the path
    +	to the folder that contains the contract definitions. In our case the
    +	path will be /groupId/artifactId/version/contracts
    +	 */
    +	contractRepository {
    +		repositoryUrl = "git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git"
    +	}
    +	// The mode can't be classpath
    +	contractsMode = "REMOTE"
    +	// Base class mappings etc.
    +}
    +
    +/*
    +In this scenario we want to publish stubs to SCM whenever
    +the `publish` task is executed
    +*/
    +publish.dependsOn("publishStubsToScm")

    +

    With such a setup:

    • Git project will be cloned to a temporary directory
    • The SCM stub downloader will go to META-INF/groupId/artifactId/version/contracts folder +to find contracts. E.g. for com.example:foo:1.0.0 the path would be +META-INF/com.example/foo/1.0.0/contracts
    • Tests will be generated from the contracts
    • Stubs will be created from the contracts
    • Once the tests pass, the stubs will be committed in the cloned repository
    • Finally, a push will be done to that repo’s origin

    88.6.3 Producer with contracts stored locally

    Another option to use the SCM as the destination for stubs and contracts is to store the contracts locally, with the producer, and only push the contracts and the stubs to SCM. Below, you can find the setup required to achieve this using Maven and Gradle.

    Maven.  +

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<!-- In the default configuration, we want to use the contracts stored locally -->
    +	<configuration>
    +		<baseClassMappings>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*messaging.*</contractPackageRegex>
    +				<baseClassFQN>com.example.BeerMessagingBase</baseClassFQN>
    +			</baseClassMapping>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*rest.*</contractPackageRegex>
    +				<baseClassFQN>com.example.BeerRestBase</baseClassFQN>
    +			</baseClassMapping>
    +		</baseClassMappings>
    +		<basePackageForTests>com.example</basePackageForTests>
    +	</configuration>
    +	<executions>
    +		<execution>
    +			<phase>package</phase>
    +			<goals>
    +				<!-- By default we will not push the stubs back to SCM,
    +				you have to explicitly add it as a goal -->
    +				<goal>pushStubsToScm</goal>
    +			</goals>
    +			<configuration>
    +				<!-- We want to pick contracts from a Git repository -->
    +				<contractsRepositoryUrl>git://file://${env.ROOT}/target/contract_empty_git/
    +				</contractsRepositoryUrl>
    +				<!-- Example of URL via git protocol -->
    +				<!--<contractsRepositoryUrl>git://git@github.com:spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>-->
    +				<!-- Example of URL via http protocol -->
    +				<!--<contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>-->
    +				<!-- We reuse the contract dependency section to set up the path
    +				to the folder that contains the contract definitions. In our case the
    +				path will be /groupId/artifactId/version/contracts -->
    +				<contractDependency>
    +					<groupId>${project.groupId}</groupId>
    +					<artifactId>${project.artifactId}</artifactId>
    +					<version>${project.version}</version>
    +				</contractDependency>
    +				<!-- The mode can't be classpath -->
    +				<contractsMode>LOCAL</contractsMode>
    +			</configuration>
    +		</execution>
    +	</executions>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		// Base package for generated tests
    +	basePackageForTests = "com.example"
    +	baseClassMappings {
    +		baseClassMapping(".*messaging.*", "com.example.BeerMessagingBase")
    +		baseClassMapping(".*rest.*", "com.example.BeerRestBase")
    +	}
    +}
    +
    +/*
    +In this scenario we want to publish stubs to SCM whenever
    +the `publish` task is executed
    +*/
    +publishStubsToScm {
    +	// We want to modify the default set up of the plugin when publish stubs to scm is called
    +	customize {
    +		// We want to pick contracts from a Git repository
    +		contractDependency {
    +			stringNotation = "${project.group}:${project.name}:${project.version}"
    +		}
    +		/*
    +		We reuse the contract dependency section to set up the path
    +		to the folder that contains the contract definitions. In our case the
    +		path will be /groupId/artifactId/version/contracts
    +		 */
    +		contractRepository {
    +			repositoryUrl = "git://file://${System.getenv("ROOT")}/target/contract_empty_git/"
    +		}
    +		// The mode can't be classpath
    +		contractsMode = "LOCAL"
    +	}
    +}
    +
    +publish.dependsOn("publishStubsToScm")
    +publishToMavenLocal.dependsOn("publishStubsToScm")

    +

    With such a setup:

    • Contracts from the default src/test/resources/contracts directory will be picked
    • Tests will be generated from the contracts
    • Stubs will be created from the contracts
    • Once the tests pass

      • Git project will be cloned to a temporary directory
      • The stubs and contracts will be committed in the cloned repository
    • Finally, a push will be done to that repo’s origin

    Keeping contracts with the producer and stubs in an external repository

    It is also possible to keep the contracts in the producer repository, but keep the stubs in an external git repo. +This is most useful when you want to use the base consumer-producer collaboration flow, but do not have a possibility to +use an artifact repository for storing the stubs.

    In order to do that, use the usual producer setup, and then add the pushStubsToScm goal and set +contractsRepositoryUrl to the repository where you want to keep the stubs.

    88.6.4 Consumer

    On the consumer side when passing the repositoryRoot parameter, +either from the @AutoConfigureStubRunner annotation, the +JUnit rule, JUnit 5 extension or properties, it’s enough to pass the URL of the +SCM repository, prefixed with the protocol. For example

    @AutoConfigureStubRunner(
    +    stubsMode="REMOTE",
    +    repositoryRoot="git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git",
    +    ids="com.example:bookstore:0.0.1.RELEASE"
    +)

    With such a setup:

    • Git project will be cloned to a temporary directory
    • The SCM stub downloader will go to META-INF/groupId/artifactId/version/ folder +to find stub definitions and contracts. E.g. for com.example:foo:1.0.0 the path would be +META-INF/com.example/foo/1.0.0/
    • Stub servers will be started and fed with mappings
    • Messaging definitions will be read and used in the messaging tests

    88.7 Can I use the Pact Broker?

    When using Pact you can use the Pact Broker +to store and share Pact definitions. Starting from Spring Cloud Contract +2.0.0 one can fetch Pact files from the Pact Broker to generate +tests and stubs.

    As a prerequisite the Pact Converter and Pact Stub Downloader +are required. You have to add them via the spring-cloud-contract-pact dependency. +You can read more about it in the Section 96.1.1, “Pact Converter” section.

    [Important]Important

    Pact follows the Consumer Contract convention. That means +that the Consumer creates the Pact definitions first, then +shares the files with the Producer. Those expectations are generated +from the Consumer’s code and can break the Producer if the expectations +are not met.

    88.7.1 Pact Consumer

    The consumer uses Pact framework to generate Pact files. The +Pact files are sent to the Pact Broker. An example of such +setup can be found here.

    88.7.2 Producer

    For the producer, to use the Pact files from the Pact Broker, we can reuse the +same mechanism we use for external contracts. We route Spring Cloud Contract +to use the Pact implementation via the URL that contains +the pact:// protocol. It’s enough to pass the URL to the +Pact Broker. An example of such setup can be found here.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <!-- Base class mappings etc. -->
    +
    +        <!-- We want to pick contracts from a Git repository -->
    +        <contractsRepositoryUrl>pact://http://localhost:8085</contractsRepositoryUrl>
    +
    +        <!-- We reuse the contract dependency section to set up the path
    +        to the folder that contains the contract definitions. In our case the
    +        path will be /groupId/artifactId/version/contracts -->
    +        <contractDependency>
    +            <groupId>${project.groupId}</groupId>
    +            <artifactId>${project.artifactId}</artifactId>
    +            <!-- When + is passed, a latest tag will be applied when fetching pacts -->
    +            <version>+</version>
    +        </contractDependency>
    +
    +        <!-- The contracts mode can't be classpath -->
    +        <contractsMode>REMOTE</contractsMode>
    +    </configuration>
    +    <!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-contract-pact</artifactId>
    +            <version>${spring-cloud-contract.version}</version>
    +        </dependency>
    +    </dependencies>
    +</plugin>

    +

    Gradle.  +

    buildscript {
    +	repositories {
    +		//...
    +	}
    +
    +	dependencies {
    +		// ...
    +		// Don't forget to add spring-cloud-contract-pact to the classpath!
    +		classpath "org.springframework.cloud:spring-cloud-contract-pact:${contractVersion}"
    +	}
    +}
    +
    +contracts {
    +	// When + is passed, a latest tag will be applied when fetching pacts
    +	contractDependency {
    +		stringNotation = "${project.group}:${project.name}:+"
    +	}
    +	contractRepository {
    +		repositoryUrl = "pact://http://localhost:8085"
    +	}
    +	// The mode can't be classpath
    +	contractsMode = "REMOTE"
    +	// Base class mappings etc.
    +}

    +

    With such a setup:

    • Pact files will be downloaded from the Pact Broker
    • Spring Cloud Contract will convert the Pact files into tests and stubs
    • The JAR with the stubs gets automatically created as usual

    88.7.3 Pact Consumer (Producer Contract approach)

    In the scenario where you don’t want to do Consumer Contract approach +(for every single consumer define the expectations) but you’d prefer +to do Producer Contracts (the producer provides the contracts and +publishes stubs), it’s enough to use Spring Cloud Contract with +Stub Runner option. An example of such setup can be found here.

    First, remember to add Stub Runner and Spring Cloud Contract Pact module +as test dependencies.

    Maven.  +

    <dependencyManagement>
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-dependencies</artifactId>
    +            <version>${spring-cloud.version}</version>
    +            <type>pom</type>
    +            <scope>import</scope>
    +        </dependency>
    +    </dependencies>
    +</dependencyManagement>
    +
    +<!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
    +<dependencies>
    +    <!-- ... -->
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +        <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-contract-pact</artifactId>
    +        <scope>test</scope>
    +    </dependency>
    +</dependencies>

    +

    Gradle.  +

    dependencyManagement {
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    +    }
    +}
    +
    +dependencies {
    +    //...
    +    testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
    +    // Don't forget to add spring-cloud-contract-pact to the classpath!
    +    testCompile("org.springframework.cloud:spring-cloud-contract-pact")
    +}

    +

    Next, just pass the URL of the Pact Broker to repositoryRoot, prefixed +with pact:// protocol. E.g. pact://http://localhost:8085

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		ids = "com.example:beer-api-producer-pact",
    +		repositoryRoot = "pact://http://localhost:8085")
    +public class BeerControllerTest {
    +    //Inject the port of the running stub
    +    @StubRunnerPort("beer-api-producer-pact") int producerPort;
    +    //...
    +}

    With such a setup:

    • Pact files will be downloaded from the Pact Broker
    • Spring Cloud Contract will convert the Pact files into stub definitions
    • The stub servers will be started and fed with stubs

    For more information about Pact support you can go to +the Section 96.7, “Using the Pact Stub Downloader” section.

    88.8 How can I debug the request/response being sent by the generated tests client?

    The generated tests all boil down to RestAssured in some form or fashion which relies on Apache HttpClient. HttpClient has a facility called wire logging which logs the entire request and response to HttpClient. Spring Boot has a logging common application property for doing this sort of thing, just add this to your application properties

    logging.level.org.apache.http.wire=DEBUG

    88.8.1 How can I debug the mapping/request/response being sent by WireMock?

    Starting from version 1.2.0 we turn on WireMock logging to +info and the WireMock notifier to being verbose. Now you will +exactly know what request was received by WireMock server and which +matching response definition was picked.

    To turn off this feature just bump WireMock logging to ERROR

    logging.level.com.github.tomakehurst.wiremock=ERROR

    88.8.2 How can I see what got registered in the HTTP server stub?

    You can use the mappingsOutputFolder property on @AutoConfigureStubRunner, StubRunnerRule or +`StubRunnerExtension`to dump all mappings per artifact id. Also the port at which the given stub server +was started will be attached.

    88.8.3 Can I reference text from file?

    Yes! With version 1.2.0 we’ve added such a possibility. It’s enough to call file(…​) method in the +DSL and provide a path relative to where the contract lays. +If you’re using YAML just use the bodyFromFile property.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract_stub_runner.html b/Greenwich.SR5/multi/multi__spring_cloud_contract_stub_runner.html new file mode 100644 index 00000000..22d778fc --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract_stub_runner.html @@ -0,0 +1,895 @@ + + + 92. Spring Cloud Contract Stub Runner

    92. Spring Cloud Contract Stub Runner

    One of the issues that you might encounter while using Spring Cloud Contract Verifier is +passing the generated WireMock JSON stubs from the server side to the client side (or to +various clients). The same takes place in terms of client-side generation for messaging.

    Copying the JSON files and setting the client side for messaging manually is out of the +question. That is why we introduced Spring Cloud Contract Stub Runner. It can +automatically download and run the stubs for you.

    92.1 Snapshot versions

    Add the additional snapshot repository to your build.gradle file to use snapshot +versions, which are automatically uploaded after every successful build:

    Maven.  +

    <repositories>
    +	<repository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +</repositories>
    +<pluginRepositories>
    +	<pluginRepository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +</pluginRepositories>

    +

    Gradle.  +

    /*
    + We need to use the [buildscript {}] section when we have to modify
    + the classpath for the plugins. If that's not the case this section
    + can be skipped.
    +
    + If you don't need to modify the classpath (e.g. add a Pact dependency),
    + then you can just set the [pluginManagement {}] section in [settings.gradle] file.
    +
    + // settings.gradle
    + pluginManagement {
    +    repositories {
    +        // for snapshots
    +        maven {url "https://repo.spring.io/snapshot"}
    +        // for milestones
    +        maven {url "https://repo.spring.io/milestone"}
    +        // for GA versions
    +        gradlePluginPortal()
    +    }
    + }
    +
    + */
    +buildscript {
    +	repositories {
    +		mavenCentral()
    +		mavenLocal()
    +		maven { url "https://repo.spring.io/snapshot" }
    +		maven { url "https://repo.spring.io/milestone" }
    +		maven { url "https://repo.spring.io/release" }
    +	}

    +

    92.2 Publishing Stubs as JARs

    The easiest approach would be to centralize the way stubs are kept. For example, you can +keep them as jars in a Maven repository.

    [Tip]Tip

    For both Maven and Gradle, the setup comes ready to work. However, you can customize +it if you want to.

    Maven.  +

    <!-- First disable the default jar setup in the properties section -->
    +<!-- we don't want the verifier to do a jar for us -->
    +<spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>
    +
    +<!-- Next add the assembly plugin to your build -->
    +<!-- we want the assembly plugin to generate the JAR -->
    +<plugin>
    +	<groupId>org.apache.maven.plugins</groupId>
    +	<artifactId>maven-assembly-plugin</artifactId>
    +	<executions>
    +		<execution>
    +			<id>stub</id>
    +			<phase>prepare-package</phase>
    +			<goals>
    +				<goal>single</goal>
    +			</goals>
    +			<inherited>false</inherited>
    +			<configuration>
    +				<attach>true</attach>
    +				<descriptors>
    +					$../../../../src/assembly/stub.xml
    +				</descriptors>
    +			</configuration>
    +		</execution>
    +	</executions>
    +</plugin>
    +
    +<!-- Finally setup your assembly. Below you can find the contents of src/main/assembly/stub.xml -->
    +<assembly
    +	xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    +	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
    +	<id>stubs</id>
    +	<formats>
    +		<format>jar</format>
    +	</formats>
    +	<includeBaseDirectory>false</includeBaseDirectory>
    +	<fileSets>
    +		<fileSet>
    +			<directory>src/main/java</directory>
    +			<outputDirectory>/</outputDirectory>
    +			<includes>
    +				<include>**com/example/model/*.*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>${project.build.directory}/classes</directory>
    +			<outputDirectory>/</outputDirectory>
    +			<includes>
    +				<include>**com/example/model/*.*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>${project.build.directory}/snippets/stubs</directory>
    +			<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory>
    +			<includes>
    +				<include>**/*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>$../../../../src/test/resources/contracts</directory>
    +			<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory>
    +			<includes>
    +				<include>**/*.groovy</include>
    +			</includes>
    +		</fileSet>
    +	</fileSets>
    +</assembly>

    +

    Gradle.  +

    ext {
    +	contractsDir = file("mappings")
    +	stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
    +}
    +
    +// Automatically added by plugin:
    +// copyContracts - copies contracts to the output folder from which JAR will be created
    +// verifierStubsJar - JAR with a provided stub suffix
    +// the presented publication is also added by the plugin but you can modify it as you wish
    +
    +publishing {
    +	publications {
    +		stubs(MavenPublication) {
    +			artifactId "${project.name}-stubs"
    +			artifact verifierStubsJar
    +		}
    +	}
    +}

    +

    92.3 Stub Runner Core

    Runs stubs for service collaborators. Treating stubs as contracts of services allows to use stub-runner as an implementation of +Consumer Driven Contracts.

    Stub Runner allows you to automatically download the stubs of the provided dependencies (or pick those from the classpath), start WireMock servers for them and feed them with proper stub definitions. +For messaging, special stub routes are defined.

    92.3.1 Retrieving stubs

    You can pick the following options of acquiring stubs

    • Aether based solution that downloads JARs with stubs from Artifactory / Nexus
    • Classpath scanning solution that searches classpath via pattern to retrieve stubs
    • Write your own implementation of the org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder for full customization

    The latter example is described in the Custom Stub Runner section.

    Stub downloading

    You can control the stub downloading via the stubsMode switch. It picks value from the +StubRunnerProperties.StubsMode enum. You can use the following options

    • StubRunnerProperties.StubsMode.CLASSPATH (default value) - will pick stubs from the classpath
    • StubRunnerProperties.StubsMode.LOCAL - will pick stubs from a local storage (e.g. .m2)
    • StubRunnerProperties.StubsMode.REMOTE - will pick stubs from a remote location

    Example:

    @AutoConfigureStubRunner(repositoryRoot="https://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095", stubsMode = StubRunnerProperties.StubsMode.LOCAL)

    Classpath scanning

    If you set the stubsMode property to StubRunnerProperties.StubsMode.CLASSPATH +(or set nothing since CLASSPATH is the default value) then classpath will get scanned. +Let’s look at the following example:

    @AutoConfigureStubRunner(ids = {
    +    "com.example:beer-api-producer:+:stubs:8095",
    +    "com.example.foo:bar:1.0.0:superstubs:8096"
    +})

    If you’ve added the dependencies to your classpath

    Maven.  +

    <dependency>
    +    <groupId>com.example</groupId>
    +    <artifactId>beer-api-producer-restdocs</artifactId>
    +    <classifier>stubs</classifier>
    +    <version>0.0.1-SNAPSHOT</version>
    +    <scope>test</scope>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>*</groupId>
    +            <artifactId>*</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>
    +<dependency>
    +    <groupId>com.example.foo</groupId>
    +    <artifactId>bar</artifactId>
    +    <classifier>superstubs</classifier>
    +    <version>1.0.0</version>
    +    <scope>test</scope>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>*</groupId>
    +            <artifactId>*</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>

    +

    Gradle.  +

    testCompile("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") {
    +    transitive = false
    +}
    +testCompile("com.example.foo:bar:1.0.0:superstubs") {
    +    transitive = false
    +}

    +

    Then the following locations on your classpath will get scanned. For com.example:beer-api-producer-restdocs

    • /META-INF/com.example/beer-api-producer-restdocs/*/.*
    • /contracts/com.example/beer-api-producer-restdocs/*/.*
    • /mappings/com.example/beer-api-producer-restdocs/*/.*

    and com.example.foo:bar

    • /META-INF/com.example.foo/bar/*/.*
    • /contracts/com.example.foo/bar/*/.*
    • /mappings/com.example.foo/bar/*/.*
    [Tip]Tip

    As you can see you have to explicitly provide the group and artifact ids when packaging the +producer stubs.

    The producer would setup the contracts like this:

    └── src
    +    └── test
    +        └── resources
    +            └── contracts
    +                └── com.example
    +                    └── beer-api-producer-restdocs
    +                        └── nested
    +                            └── contract3.groovy

    To achieve proper stub packaging.

    Or using the Maven assembly plugin or +Gradle Jar task you have to create the following +structure in your stubs jar.

    └── META-INF
    +    └── com.example
    +        └── beer-api-producer-restdocs
    +            └── 2.0.0
    +                ├── contracts
    +                │   └── nested
    +                │       └── contract2.groovy
    +                └── mappings
    +                    └── mapping.json

    By maintaining this structure classpath gets scanned and you can profit from the messaging / +HTTP stubs without the need to download artifacts.

    Configuring HTTP Server Stubs

    Stub Runner has a notion of a HttpServerStub that abstracts the underlaying +concrete implementation of the HTTP server (e.g. WireMock is one of the implementations). +Sometimes, you need to perform some additional tuning of the stub servers, +that is concrete for the given implementation. To do that, Stub Runner gives you +the httpServerStubConfigurer property that is available in the annotation, +JUnit rule, and is accessible via system properties, where you can provide +your implementation of the org.springframework.cloud.contract.stubrunner.HttpServerStubConfigurer interface. The implementations can alter +the configuration files for the given HTTP server stub.

    Spring Cloud Contract Stub Runner comes with an implementation that you +can extend, for WireMock - org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStubConfigurer. In the configure method +you can provide your own, custom configuration for the given stub. The use +case might be starting WireMock for the given artifact id, on an HTTPs port. Example:

    WireMockHttpServerStubConfigurer implementation.  +

    @CompileStatic
    +static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {
    +
    +	private static final Log log = LogFactory.getLog(HttpsForFraudDetection)
    +
    +	@Override
    +	WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
    +		if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
    +			int httpsPort = SocketUtils.findAvailableTcpPort()
    +			log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
    +			return httpStubConfiguration
    +					.httpsPort(httpsPort)
    +		}
    +		return httpStubConfiguration
    +	}
    +}

    +

    You can then reuse it via the annotation

    @AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
    +		httpServerStubConfigurer = HttpsForFraudDetection)

    Whenever an https port is found, it will take precedence over the http one.

    92.3.2 Running stubs

    Running using main app

    You can set the following options to the main class:

    -c, --classifier                Suffix for the jar containing stubs (e.
    +                                  g. 'stubs' if the stub jar would
    +                                  have a 'stubs' classifier for stubs:
    +                                  foobar-stubs ). Defaults to 'stubs'
    +                                  (default: stubs)
    +--maxPort, --maxp <Integer>     Maximum port value to be assigned to
    +                                  the WireMock instance. Defaults to
    +                                  15000 (default: 15000)
    +--minPort, --minp <Integer>     Minimum port value to be assigned to
    +                                  the WireMock instance. Defaults to
    +                                  10000 (default: 10000)
    +-p, --password                  Password to user when connecting to
    +                                  repository
    +--phost, --proxyHost            Proxy host to use for repository
    +                                  requests
    +--pport, --proxyPort [Integer]  Proxy port to use for repository
    +                                  requests
    +-r, --root                      Location of a Jar containing server
    +                                  where you keep your stubs (e.g. http:
    +                                  //nexus.
    +                                  net/content/repositories/repository)
    +-s, --stubs                     Comma separated list of Ivy
    +                                  representation of jars with stubs.
    +                                  Eg. groupid:artifactid1,groupid2:
    +                                  artifactid2:classifier
    +--sm, --stubsMode               Stubs mode to be used. Acceptable values
    +                                  [CLASSPATH, LOCAL, REMOTE]
    +-u, --username                  Username to user when connecting to
    +                                  repository

    HTTP Stubs

    Stubs are defined in JSON documents, whose syntax is defined in WireMock documentation

    Example:

    {
    +    "request": {
    +        "method": "GET",
    +        "url": "/ping"
    +    },
    +    "response": {
    +        "status": 200,
    +        "body": "pong",
    +        "headers": {
    +            "Content-Type": "text/plain"
    +        }
    +    }
    +}

    Viewing registered mappings

    Every stubbed collaborator exposes list of defined mappings under __/admin/ endpoint.

    You can also use the mappingsOutputFolder property to dump the mappings to files. + For annotation based approach it would look like this

    @AutoConfigureStubRunner(ids="a.b.c:loanIssuance,a.b.c:fraudDetectionServer",
    +mappingsOutputFolder = "target/outputmappings/")

    and for the JUnit approach like this:

    @ClassRule @Shared StubRunnerRule rule = new StubRunnerRule()
    +			.repoRoot("http://some_url")
    +			.downloadStub("a.b.c", "loanIssuance")
    +			.downloadStub("a.b.c:fraudDetectionServer")
    +			.withMappingsOutputFolder("target/outputmappings")

    Then if you check out the folder target/outputmappings you would see the following structure

    .
    +├── fraudDetectionServer_13705
    +└── loanIssuance_12255

    That means that there were two stubs registered. fraudDetectionServer was registered at port 13705 +and loanIssuance at port 12255. If we take a look at one of the files we would see (for WireMock) +mappings available for the given server:

    [{
    +  "id" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7",
    +  "request" : {
    +    "url" : "/name",
    +    "method" : "GET"
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "fraudDetectionServer"
    +  },
    +  "uuid" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7"
    +},
    +...
    +]

    Messaging Stubs

    Depending on the provided Stub Runner dependency and the DSL the messaging routes are automatically set up.

    92.4 Stub Runner JUnit Rule and Stub Runner JUnit5 Extension

    Stub Runner comes with a JUnit rule thanks to which you can very easily download and run stubs for given group and artifact id:

    @ClassRule
    +public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot())
    +		.stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")
    +		.downloadStub(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer");
    +
    +@BeforeClass
    +@AfterClass
    +public static void setupProps() {
    +	System.clearProperty("stubrunner.repository.root");
    +	System.clearProperty("stubrunner.classifier");
    +}

    There’s also a StubRunnerExtension available for JUnit 5. StubRunnerRule and StubRunnerExtension work in a very +similar fashion. After the rule/ extension is executed, Stub Runner connects to your Maven repository and for the given list of dependencies tries to:

    • download them
    • cache them locally
    • unzip them to a temporary folder
    • start a WireMock server for each Maven dependency on a random port from the provided range of ports / provided port
    • feed the WireMock server with all JSON files that are valid WireMock definitions
    • can also send messages (remember to pass an implementation of MessageVerifier interface)

    Stub Runner uses Eclipse Aether mechanism to download the Maven dependencies. +Check their docs for more information.

    Since the StubRunnerRule and StubRunnerExtension implement the StubFinder they allow you to find the started stubs:

    package org.springframework.cloud.contract.stubrunner;
    +
    +import java.net.URL;
    +import java.util.Collection;
    +import java.util.Map;
    +
    +import org.springframework.cloud.contract.spec.Contract;
    +
    +/**
    + * Contract for finding registered stubs.
    + *
    + * @author Marcin Grzejszczak
    + */
    +public interface StubFinder extends StubTrigger {
    +
    +	/**
    +	 * For the given groupId and artifactId tries to find the matching URL of the running
    +	 * stub.
    +	 * @param groupId - might be null. In that case a search only via artifactId takes
    +	 * place
    +	 * @param artifactId - artifact id of the stub
    +	 * @return URL of a running stub or throws exception if not found
    +	 * @throws StubNotFoundException in case of not finding a stub
    +	 */
    +	URL findStubUrl(String groupId, String artifactId) throws StubNotFoundException;
    +
    +	/**
    +	 * For the given Ivy notation {@code [groupId]:artifactId:[version]:[classifier]}
    +	 * tries to find the matching URL of the running stub. You can also pass only
    +	 * {@code artifactId}.
    +	 * @param ivyNotation - Ivy representation of the Maven artifact
    +	 * @return URL of a running stub or throws exception if not found
    +	 * @throws StubNotFoundException in case of not finding a stub
    +	 */
    +	URL findStubUrl(String ivyNotation) throws StubNotFoundException;
    +
    +	/**
    +	 * @return all running stubs
    +	 */
    +	RunningStubs findAllRunningStubs();
    +
    +	/**
    +	 * @return the list of Contracts
    +	 */
    +	Map<StubConfiguration, Collection<Contract>> getContracts();
    +
    +}

    Example of usage in Spock tests:

    @ClassRule
    +@Shared
    +StubRunnerRule rule = new StubRunnerRule()
    +		.stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.repoRoot(StubRunnerRuleSpec.getResource("/m2repo/repository").toURI().toString())
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
    +		.withMappingsOutputFolder("target/outputmappingsforrule")
    +
    +
    +def 'should start WireMock servers'() {
    +	expect: 'WireMocks are running'
    +		rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
    +		rule.findStubUrl('loanIssuance') != null
    +		rule.findStubUrl('loanIssuance') == rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
    +		rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
    +	and:
    +		rule.findAllRunningStubs().isPresent('loanIssuance')
    +		rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
    +		rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
    +	and: 'Stubs were registered'
    +		"${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
    +		"${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
    +}
    +
    +def 'should output mappings to output folder'() {
    +	when:
    +		def url = rule.findStubUrl('fraudDetectionServer')
    +	then:
    +		new File("target/outputmappingsforrule", "fraudDetectionServer_${url.port}").exists()
    +}

    Example of usage in JUnit tests:

    	@Test
    +	public void should_start_wiremock_servers() throws Exception {
    +		// expect: 'WireMocks are running'
    +		then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")).isNotNull();
    +		then(rule.findStubUrl("loanIssuance")).isNotNull();
    +		then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl(
    +				"org.springframework.cloud.contract.verifier.stubs", "loanIssuance"));
    +		then(rule.findStubUrl(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer"))
    +						.isNotNull();
    +		// and:
    +		then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue();
    +		then(rule.findAllRunningStubs().isPresent(
    +				"org.springframework.cloud.contract.verifier.stubs",
    +				"fraudDetectionServer")).isTrue();
    +		then(rule.findAllRunningStubs().isPresent(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer"))
    +						.isTrue();
    +		// and: 'Stubs were registered'
    +		then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name"))
    +				.isEqualTo("loanIssuance");
    +		then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name"))
    +				.isEqualTo("fraudDetectionServer");
    +	}
    +
    +	private String httpGet(String url) throws Exception {
    +		try (InputStream stream = URI.create(url).toURL().openStream()) {
    +			return StreamUtils.copyToString(stream, Charset.forName("UTF-8"));
    +		}
    +	}
    +
    +}

    JUnit 5 Extension example:

    // Visible for Junit
    +@RegisterExtension
    +static StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
    +		.repoRoot(repoRoot()).stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")
    +		.downloadStub(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
    +		.withMappingsOutputFolder("target/outputmappingsforrule");
    +
    +@BeforeAll
    +@AfterAll
    +static void setupProps() {
    +	System.clearProperty("stubrunner.repository.root");
    +	System.clearProperty("stubrunner.classifier");
    +}
    +
    +private static String repoRoot() {
    +	try {
    +		return StubRunnerRuleJUnitTest.class.getResource("/m2repo/repository/")
    +				.toURI().toString();
    +	}
    +	catch (Exception e) {
    +		return "";
    +	}
    +}

    Check the Common properties for JUnit and Spring for more information on how to apply global configuration of Stub Runner.

    [Important]Important

    To use the JUnit rule or JUnit 5 extension together with messaging, you have to provide an implementation of the +MessageVerifier interface to the rule builder (e.g. rule.messageVerifier(new MyMessageVerifier())). +If you don’t do this, then whenever you try to send a message an exception will be thrown.

    92.4.1 Maven settings

    The stub downloader honors Maven settings for a different local repository folder. +Authentication details for repositories and profiles are currently not taken into account, so you need to specify it using the properties mentioned above.

    92.4.2 Providing fixed ports

    You can also run your stubs on fixed ports. You can do it in two different ways. One is to pass it in the properties, and the other via fluent API of +JUnit rule.

    92.4.3 Fluent API

    When using the StubRunnerRule or StubRunnerExtension you can add a stub to download and then pass the port for the last downloaded stub.

    @ClassRule
    +public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot())
    +		.stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")
    +		.withPort(12345).downloadStub(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer:12346");
    +
    +@BeforeClass
    +@AfterClass
    +public static void setupProps() {
    +	System.clearProperty("stubrunner.repository.root");
    +	System.clearProperty("stubrunner.classifier");
    +}

    You can see that for this example the following test is valid:

    then(rule.findStubUrl("loanIssuance"))
    +		.isEqualTo(URI.create("http://localhost:12345").toURL());
    +then(rule.findStubUrl("fraudDetectionServer"))
    +		.isEqualTo(URI.create("http://localhost:12346").toURL());

    92.4.4 Stub Runner with Spring

    Sets up Spring configuration of the Stub Runner project.

    By providing a list of stubs inside your configuration file the Stub Runner automatically downloads +and registers in WireMock the selected stubs.

    If you want to find the URL of your stubbed dependency you can autowire the StubFinder interface and use +its methods as presented below:

    @ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
    +@SpringBootTest(properties = [" stubrunner.cloud.enabled=false",
    +		'foo=${stubrunner.runningstubs.fraudDetectionServer.port}',
    +		'fooWithGroup=${stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port}'])
    +@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
    +		httpServerStubConfigurer = HttpsForFraudDetection)
    +@ActiveProfiles("test")
    +class StubRunnerConfigurationSpec extends Specification {
    +
    +	@Autowired
    +	StubFinder stubFinder
    +	@Autowired
    +	Environment environment
    +	@StubRunnerPort("fraudDetectionServer")
    +	int fraudDetectionServerPort
    +	@StubRunnerPort("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
    +	int fraudDetectionServerPortWithGroupId
    +	@Value('${foo}')
    +	Integer foo
    +
    +	@BeforeClass
    +	@AfterClass
    +	void setupProps() {
    +		System.clearProperty("stubrunner.repository.root")
    +		System.clearProperty("stubrunner.classifier")
    +		WireMockHttpServerStubAccessor.clear()
    +	}
    +
    +	def 'should mark all ports as random'() {
    +		expect:
    +			WireMockHttpServerStubAccessor.everyPortRandom()
    +	}
    +
    +	def 'should start WireMock servers'() {
    +		expect: 'WireMocks are running'
    +			stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
    +			stubFinder.findStubUrl('loanIssuance') != null
    +			stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
    +			stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance')
    +			stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs')
    +			stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
    +		and:
    +			stubFinder.findAllRunningStubs().isPresent('loanIssuance')
    +			stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
    +			stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
    +		and: 'Stubs were registered'
    +			"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
    +			"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
    +		and: 'Fraud Detection is an HTTPS endpoint'
    +			stubFinder.findStubUrl('fraudDetectionServer').toString().startsWith("https")
    +	}
    +
    +	def 'should throw an exception when stub is not found'() {
    +		when:
    +			stubFinder.findStubUrl('nonExistingService')
    +		then:
    +			thrown(StubNotFoundException)
    +		when:
    +			stubFinder.findStubUrl('nonExistingGroupId', 'nonExistingArtifactId')
    +		then:
    +			thrown(StubNotFoundException)
    +	}
    +
    +	def 'should register started servers as environment variables'() {
    +		expect:
    +			environment.getProperty("stubrunner.runningstubs.loanIssuance.port") != null
    +			stubFinder.findAllRunningStubs().getPort("loanIssuance") == (environment.getProperty("stubrunner.runningstubs.loanIssuance.port") as Integer)
    +		and:
    +			environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
    +			stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") as Integer)
    +		and:
    +			environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
    +			stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port") as Integer)
    +	}
    +
    +	def 'should be able to interpolate a running stub in the passed test property'() {
    +		given:
    +			int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
    +		expect:
    +			fraudPort > 0
    +			environment.getProperty("foo", Integer) == fraudPort
    +			environment.getProperty("fooWithGroup", Integer) == fraudPort
    +			foo == fraudPort
    +	}
    +
    +	@Issue("#573")
    +	def 'should be able to retrieve the port of a running stub via an annotation'() {
    +		given:
    +			int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
    +		expect:
    +			fraudPort > 0
    +			fraudDetectionServerPort == fraudPort
    +			fraudDetectionServerPortWithGroupId == fraudPort
    +	}
    +
    +	def 'should dump all mappings to a file'() {
    +		when:
    +			def url = stubFinder.findStubUrl("fraudDetectionServer")
    +		then:
    +			new File("target/outputmappings/", "fraudDetectionServer_${url.port}").exists()
    +	}
    +
    +	@Configuration
    +	@EnableAutoConfiguration
    +	static class Config {}
    +
    +	@CompileStatic
    +	static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {
    +
    +		private static final Log log = LogFactory.getLog(HttpsForFraudDetection)
    +
    +		@Override
    +		WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
    +			if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
    +				int httpsPort = SocketUtils.findAvailableTcpPort()
    +				log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
    +				return httpStubConfiguration
    +						.httpsPort(httpsPort)
    +			}
    +			return httpStubConfiguration
    +		}
    +	}
    +}

    for the following configuration file:

    stubrunner:
    +  repositoryRoot: classpath:m2repo/repository/
    +  ids:
    +    - org.springframework.cloud.contract.verifier.stubs:loanIssuance
    +    - org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer
    +    - org.springframework.cloud.contract.verifier.stubs:bootService
    +  stubs-mode: remote

    Instead of using the properties you can also use the properties inside the @AutoConfigureStubRunner. +Below you can find an example of achieving the same result by setting values on the annotation.

    @AutoConfigureStubRunner(
    +		ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
    +				"org.springframework.cloud.contract.verifier.stubs:bootService"],
    +		stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		repositoryRoot = "classpath:m2repo/repository/")

    Stub Runner Spring registers environment variables in the following manner +for every registered WireMock server. Example for Stub Runner ids + com.example:foo, com.example:bar.

    • stubrunner.runningstubs.foo.port
    • stubrunner.runningstubs.com.example.foo.port
    • stubrunner.runningstubs.bar.port
    • stubrunner.runningstubs.com.example.bar.port

    Which you can reference in your code.

    You can also use the @StubRunnerPort annotation to inject the port of a running stub. +Value of the annotation can be the groupid:artifactid or just the artifactid. Example for Stub Runner ids +com.example:foo, com.example:bar.

    @StubRunnerPort("foo")
    +int fooPort;
    +@StubRunnerPort("com.example:bar")
    +int barPort;

    92.5 Stub Runner Spring Cloud

    Stub Runner can integrate with Spring Cloud.

    For real life examples you can check the

    92.5.1 Stubbing Service Discovery

    The most important feature of Stub Runner Spring Cloud is the fact that it’s stubbing

    • DiscoveryClient
    • Ribbon ServerList

    that means that regardless of the fact whether you’re using Zookeeper, Consul, Eureka or anything else, you don’t need that in your tests. +We’re starting WireMock instances of your dependencies and we’re telling your application whenever you’re using Feign, load balanced RestTemplate +or DiscoveryClient directly, to call those stubbed servers instead of calling the real Service Discovery tool.

    For example this test will pass

    def 'should make service discovery work'() {
    +	expect: 'WireMocks are running'
    +		"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
    +		"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
    +	and: 'Stubs can be reached via load service discovery'
    +		restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
    +		restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
    +}

    for the following configuration file

    stubrunner:
    +  idsToServiceIds:
    +    ivyNotation: someValueInsideYourCode
    +    fraudDetectionServer: someNameThatShouldMapFraudDetectionServer

    Test profiles and service discovery

    In your integration tests you typically don’t want to call neither a discovery service (e.g. Eureka) +or Config Server. That’s why you create an additional test configuration in which you want to disable +these features.

    Due to certain limitations of spring-cloud-commons to achieve this you have disable these properties +via a static block like presented below (example for Eureka)

        //Hack to work around https://github.com/spring-cloud/spring-cloud-commons/issues/156
    +    static {
    +        System.setProperty("eureka.client.enabled", "false");
    +        System.setProperty("spring.cloud.config.failFast", "false");
    +    }

    92.5.2 Additional Configuration

    You can match the artifactId of the stub with the name of your app by using the stubrunner.idsToServiceIds: map. +You can disable Stub Runner Ribbon support by providing: stubrunner.cloud.ribbon.enabled equal to false +You can disable Stub Runner support by providing: stubrunner.cloud.enabled equal to false

    [Tip]Tip

    By default all service discovery will be stubbed. That means that regardless of the fact if you have +an existing DiscoveryClient its results will be ignored. However, if you want to reuse it, just set + stubrunner.cloud.delegate.enabled to true and then your existing DiscoveryClient results will be + merged with the stubbed ones.

    The default Maven configuration used by Stub Runner can be tweaked either +via the following system properties or environment variables

    • maven.repo.local - path to the custom maven local repository location
    • org.apache.maven.user-settings - path to custom maven user settings location
    • org.apache.maven.global-settings - path to maven global settings location

    92.6 Stub Runner Boot Application

    Spring Cloud Contract Stub Runner Boot is a Spring Boot application that exposes REST endpoints to +trigger the messaging labels and to access started WireMock servers.

    One of the use-cases is to run some smoke (end to end) tests on a deployed application. +You can check out the Spring Cloud Pipelines +project for more information.

    92.6.1 How to use it?

    Stub Runner Server

    Just add the

    compile "org.springframework.cloud:spring-cloud-starter-stub-runner"

    Annotate a class with @EnableStubRunnerServer, build a fat-jar and you’re ready to go!

    For the properties check the Stub Runner Spring section.

    Stub Runner Server Fat Jar

    You can download a standalone JAR from Maven (e.g. for version 2.0.1.RELEASE), as follows:

    $ wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.0.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.0.1.RELEASE.jar'
    +$ java -jar stub-runner.jar --stubrunner.ids=... --stubrunner.repositoryRoot=...

    Spring Cloud CLI

    Starting from 1.4.0.RELEASE version of the Spring Cloud CLI +project you can start Stub Runner Boot by executing spring cloud stubrunner.

    In order to pass the configuration just create a stubrunner.yml file in the current working directory +or a subdirectory called config or in ~/.spring-cloud. The file could look like this +(example for running stubs installed locally)

    stubrunner.yml.  +

    stubrunner:
    +  stubsMode: LOCAL
    +  ids:
    +    - com.example:beer-api-producer:+:9876

    +

    and then just call spring cloud stubrunner from your terminal window to start +the Stub Runner server. It will be available at port 8750.

    92.6.2 Endpoints

    HTTP

    • GET /stubs - returns a list of all running stubs in ivy:integer notation
    • GET /stubs/{ivy} - returns a port for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    Messaging

    For Messaging

    • GET /triggers - returns a list of all running labels in ivy : [ label1, label2 …​] notation
    • POST /triggers/{label} - executes a trigger with label
    • POST /triggers/{ivy}/{label} - executes a trigger with label for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    92.6.3 Example

    @ContextConfiguration(classes = StubRunnerBoot, loader = SpringBootContextLoader)
    +@SpringBootTest(properties = "spring.cloud.zookeeper.enabled=false")
    +@ActiveProfiles("test")
    +class StubRunnerBootSpec extends Specification {
    +
    +	@Autowired
    +	StubRunning stubRunning
    +
    +	def setup() {
    +		RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
    +				new TriggerController(stubRunning))
    +	}
    +
    +	def 'should return a list of running stub servers in "full ivy:port" notation'() {
    +		when:
    +			String response = RestAssuredMockMvc.get('/stubs').body.asString()
    +		then:
    +			def root = new JsonSlurper().parseText(response)
    +			root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer
    +	}
    +
    +	def 'should return a port on which a [#stubId] stub is running'() {
    +		when:
    +			def response = RestAssuredMockMvc.get("/stubs/${stubId}")
    +		then:
    +			response.statusCode == 200
    +			Integer.valueOf(response.body.asString()) > 0
    +		where:
    +			stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs',
    +					   'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs',
    +					   'org.springframework.cloud.contract.verifier.stubs:bootService:+',
    +					   'org.springframework.cloud.contract.verifier.stubs:bootService',
    +					   'bootService']
    +	}
    +
    +	def 'should return 404 when missing stub was called'() {
    +		when:
    +			def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
    +		then:
    +			response.statusCode == 404
    +	}
    +
    +	def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
    +		when:
    +			String response = RestAssuredMockMvc.get('/triggers').body.asString()
    +		then:
    +			def root = new JsonSlurper().parseText(response)
    +			root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book", "return_book_1", "return_book_2"])
    +	}
    +
    +	def 'should trigger a messaging label'() {
    +		given:
    +			StubRunning stubRunning = Mock()
    +			RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
    +		when:
    +			def response = RestAssuredMockMvc.post("/triggers/delete_book")
    +		then:
    +			response.statusCode == 200
    +		and:
    +			1 * stubRunning.trigger('delete_book')
    +	}
    +
    +	def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() {
    +		given:
    +			StubRunning stubRunning = Mock()
    +			RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
    +		when:
    +			def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book")
    +		then:
    +			response.statusCode == 200
    +		and:
    +			1 * stubRunning.trigger(stubId, 'delete_book')
    +		where:
    +			stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService']
    +	}
    +
    +	def 'should throw exception when trigger is missing'() {
    +		when:
    +			RestAssuredMockMvc.post("/triggers/missing_label")
    +		then:
    +			Exception e = thrown(Exception)
    +			e.message.contains("Exception occurred while trying to return [missing_label] label.")
    +			e.message.contains("Available labels are")
    +			e.message.contains("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]")
    +			e.message.contains("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=")
    +	}
    +
    +}

    92.6.4 Stub Runner Boot with Service Discovery

    One of the possibilities of using Stub Runner Boot is to use it as a feed of stubs for "smoke-tests". What does it mean? + Let’s assume that you don’t want to deploy 50 microservice to a test environment in order + to check if your application is working fine. You’ve already executed a suite of tests during the build process + but you would also like to ensure that the packaging of your application is fine. What you can do + is to deploy your application to an environment, start it and run a couple of tests on it to see if + it’s working fine. We can call those tests smoke-tests since their idea is to check only a handful + of testing scenarios.

    The problem with this approach is such that if you’re doing microservices most likely you’re + using a service discovery tool. Stub Runner Boot allows you to solve this issue by starting the + required stubs and register them in a service discovery tool. Let’s take a look at an example of + such a setup with Eureka. Let’s assume that Eureka was already running.

    @SpringBootApplication
    +@EnableStubRunnerServer
    +@EnableEurekaClient
    +@AutoConfigureStubRunner
    +public class StubRunnerBootEurekaExample {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(StubRunnerBootEurekaExample.class, args);
    +	}
    +
    +}

    As you can see we want to start a Stub Runner Boot server @EnableStubRunnerServer, enable Eureka client @EnableEurekaClient +and we want to have the stub runner feature turned on @AutoConfigureStubRunner.

    Now let’s assume that we want to start this application so that the stubs get automatically registered. + We can do it by running the app java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar where + ${SYSTEM_PROPS} would contain the following list of properties

    * -Dstubrunner.repositoryRoot=https://repo.spring.io/snapshot (1)
    +* -Dstubrunner.cloud.stubbed.discovery.enabled=false (2)
    +* -Dstubrunner.ids=org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.
    +* springframework.cloud.contract.verifier.stubs:fraudDetectionServer,org.springframework.
    +* cloud.contract.verifier.stubs:bootService (3)
    +* -Dstubrunner.idsToServiceIds.fraudDetectionServer=
    +* someNameThatShouldMapFraudDetectionServer (4)
    +*
    +* (1) - we tell Stub Runner where all the stubs reside (2) - we don't want the default
    +* behaviour where the discovery service is stubbed. That's why the stub registration will
    +* be picked (3) - we provide a list of stubs to download (4) - we provide a list of

    That way your deployed application can send requests to started WireMock servers via the service +discovery. Most likely points 1-3 could be set by default in application.yml cause they are not +likely to change. That way you can provide only the list of stubs to download whenever you start +the Stub Runner Boot.

    92.7 Stubs Per Consumer

    There are cases in which 2 consumers of the same endpoint want to have 2 different responses.

    [Tip]Tip

    This approach also allows you to immediately know which consumer is using which part of your API. +You can remove part of a response that your API produces and you can see which of your autogenerated tests +fails. If none fails then you can safely delete that part of the response cause nobody is using it.

    Let’s look at the following example for contract defined for the producer called producer. +There are 2 consumers: foo-consumer and bar-consumer.

    Consumer foo-service

    request {
    +   url '/foo'
    +   method GET()
    +}
    +response {
    +    status OK()
    +    body(
    +       foo: "foo"
    +    }
    +}

    Consumer bar-service

    request {
    +   url '/foo'
    +   method GET()
    +}
    +response {
    +    status OK()
    +    body(
    +       bar: "bar"
    +    }
    +}

    You can’t produce for the same request 2 different responses. That’s why you can properly package the +contracts and then profit from the stubsPerConsumer feature.

    On the producer side the consumers can have a folder that contains contracts related only to them. +By setting the stubrunner.stubs-per-consumer flag to true we no longer register all stubs but only those that +correspond to the consumer application’s name. In other words we’ll scan the path of every stub and +if it contains the subfolder with name of the consumer in the path only then will it get registered.

    On the foo producer side the contracts would look like this

    .
    +└── contracts
    +    ├── bar-consumer
    +    │   ├── bookReturnedForBar.groovy
    +    │   └── shouldCallBar.groovy
    +    └── foo-consumer
    +        ├── bookReturnedForFoo.groovy
    +        └── shouldCallFoo.groovy

    Being the bar-consumer consumer you can either set the spring.application.name or the stubrunner.consumer-name to bar-consumer +Or set the test as follows:

    @ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
    +@SpringBootTest(properties = ["spring.application.name=bar-consumer"])
    +@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
    +		repositoryRoot = "classpath:m2repo/repository/",
    +		stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		stubsPerConsumer = true)
    +class StubRunnerStubsPerConsumerSpec extends Specification {
    +...
    +}

    Then only the stubs registered under a path that contains the bar-consumer in its name (i.e. those from the +src/test/resources/contracts/bar-consumer/some/contracts/…​ folder) will be allowed to be referenced.

    Or set the consumer name explicitly

    @ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
    +@SpringBootTest
    +@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
    +		repositoryRoot = "classpath:m2repo/repository/",
    +		consumerName = "foo-consumer",
    +		stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		stubsPerConsumer = true)
    +class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification {
    +...
    +}

    Then only the stubs registered under a path that contains the foo-consumer in its name (i.e. those from the +src/test/resources/contracts/foo-consumer/some/contracts/…​ folder) will be allowed to be referenced.

    You can check out issue 224 for more +information about the reasons behind this change.

    92.8 Common

    This section briefly describes common properties, including:

    92.8.1 Common Properties for JUnit and Spring

    You can set repetitive properties by using system properties or Spring configuration +properties. Here are their names with their default values:

    Property nameDefault valueDescription

    stubrunner.minPort

    10000

    Minimum value of a port for a started WireMock with stubs.

    stubrunner.maxPort

    15000

    Maximum value of a port for a started WireMock with stubs.

    stubrunner.repositoryRoot

     

    Maven repo URL. If blank, then call the local maven repo.

    stubrunner.classifier

    stubs

    Default classifier for the stub artifacts.

    stubrunner.stubsMode

    CLASSPATH

    The way you want to fetch and register the stubs

    stubrunner.ids

     

    Array of Ivy notation stubs to download.

    stubrunner.username

     

    Optional username to access the tool that stores the JARs with +stubs.

    stubrunner.password

     

    Optional password to access the tool that stores the JARs with +stubs.

    stubrunner.stubsPerConsumer

    false

    Set to true if you want to use different stubs for +each consumer instead of registering all stubs for every consumer.

    stubrunner.consumerName

     

    If you want to use a stub for each consumer and want to +override the consumer name just change this value.

    92.8.2 Stub Runner Stubs IDs

    You can provide the stubs to download via the stubrunner.ids system property. They +follow this pattern:

    groupId:artifactId:version:classifier:port

    Note that version, classifier and port are optional.

    • If you do not provide the port, a random one will be picked.
    • If you do not provide the classifier, the default is used. (Note that you can +pass an empty classifier this way: groupId:artifactId:version:).
    • If you do not provide the version, then the + will be passed and the latest one is +downloaded.

    port means the port of the WireMock server.

    [Important]Important

    Starting with version 1.0.4, you can provide a range of versions that you +would like the Stub Runner to take into consideration. You can read more about the +Aether versioning +ranges here.

    92.9 Stub Runner Docker

    We’re publishing a spring-cloud/spring-cloud-contract-stub-runner Docker image +that will start the standalone version of Stub Runner.

    If you want to learn more about the basics of Maven, artifact ids, +group ids, classifiers and Artifact Managers, just click here Section 90.16, “Docker Project”.

    92.9.1 How to use it

    Just execute the docker image. You can pass any of the Section 92.8.1, “Common Properties for JUnit and Spring” +as environment variables. The convention is that all the +letters should be upper case. The camel case notation should +and the dot (.) should be separated via underscore (_). E.g. + the stubrunner.repositoryRoot property should be represented + as a STUBRUNNER_REPOSITORY_ROOT environment variable.

    92.9.2 Example of client side usage in a non JVM project

    We’d like to use the stubs created in this Section 90.16.4, “Server side (nodejs)” step. +Let’s assume that we want to run the stubs on port 9876. The NodeJS code +is available here:

    $ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
    +$ cd bookstore

    Let’s run the Stub Runner Boot application with the stubs.

    # Provide the Spring Cloud Contract Docker version
    +$ SC_CONTRACT_DOCKER_VERSION="..."
    +# The IP at which the app is running and Docker container can reach it
    +$ APP_IP="192.168.0.100"
    +# Spring Cloud Contract Stub Runner properties
    +$ STUBRUNNER_PORT="8083"
    +# Stub coordinates 'groupId:artifactId:version:classifier:port'
    +$ STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876"
    +$ STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local"
    +# Run the docker with Stub Runner Boot
    +$ docker run  --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" -e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" -e "STUBRUNNER_STUBS_MODE=REMOTE" -p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"

    What’s happening is that

    • a standalone Stub Runner application got started
    • it downloaded the stub with coordinates com.example:bookstore:0.0.1.RELEASE:stubs on port 9876
    • it got downloaded from Artifactory running at http://192.168.0.100:8081/artifactory/libs-release-local
    • after a while Stub Runner will be running on port 8083
    • and the stubs will be running at port 9876

    On the server side we built a stateful stub. Let’s use curl to assert +that the stubs are setup properly.

    # let's execute the first request (no response is returned)
    +$ curl -H "Content-Type:application/json" -X POST --data '{ "title" : "Title", "genre" : "Genre", "description" : "Description", "author" : "Author", "publisher" : "Publisher", "pages" : 100, "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg", "buy_url" : "https://pivotal.io" }' http://localhost:9876/api/books
    +# Now time for the second request
    +$ curl -X GET http://localhost:9876/api/books
    +# You will receive contents of the JSON
    [Important]Important

    If you want use the stubs that you have built locally, on your host, +then you should pass the environment variable -e STUBRUNNER_STUBS_MODE=LOCAL and mount +the volume of your local m2 -v "${HOME}/.m2/:/root/.m2:ro"

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_introduction.html b/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_introduction.html new file mode 100644 index 00000000..a4ffb8bc --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_introduction.html @@ -0,0 +1,843 @@ + + + 87. Spring Cloud Contract Verifier Introduction

    87. Spring Cloud Contract Verifier Introduction

    Spring Cloud Contract Verifier enables Consumer Driven Contract (CDC) development of +JVM-based applications. It moves TDD to the level of software architecture.

    Spring Cloud Contract Verifier ships with Contract Definition Language (CDL). Contract +definitions are used to produce the following resources:

    • JSON stub definitions to be used by WireMock when doing integration testing on the +client code (client tests). Test code must still be written by hand, and test data is +produced by Spring Cloud Contract Verifier.
    • Messaging routes, if you’re using a messaging service. We integrate with Spring +Integration, Spring Cloud Stream, Spring AMQP, and Apache Camel. You can also set your +own integrations.
    • Acceptance tests (in JUnit 4, JUnit 5 or Spock) are used to verify if server-side implementation +of the API is compliant with the contract (server tests). A full test is generated by +Spring Cloud Contract Verifier.

    87.1 History

    Before becoming Spring Cloud Contract, this project was called Accurest. +It was created by Marcin Grzejszczak and Jakub Kubrynski +from (Codearte.

    The 0.1.0 release took place on 26 Jan 2015 and it became stable with 1.0.0 release on 29 Feb 2016.

    87.2 Why a Contract Verifier?

    Assume that we have a system consisting of multiple microservices:

    Microservices Architecture

    87.2.1 Testing issues

    If we wanted to test the application in top left corner to determine whether it can +communicate with other services, we could do one of two things:

    • Deploy all microservices and perform end-to-end tests.
    • Mock other microservices in unit/integration tests.

    Both have their advantages but also a lot of disadvantages.

    Deploy all microservices and perform end to end tests

    Advantages:

    • Simulates production.
    • Tests real communication between services.

    Disadvantages:

    • To test one microservice, we have to deploy 6 microservices, a couple of databases, +etc.
    • The environment where the tests run is locked for a single suite of tests (nobody else +would be able to run the tests in the meantime).
    • They take a long time to run.
    • The feedback comes very late in the process.
    • They are extremely hard to debug.

    Mock other microservices in unit/integration tests

    Advantages:

    • They provide very fast feedback.
    • They have no infrastructure requirements.

    Disadvantages:

    • The implementor of the service creates stubs that might have nothing to do with +reality.
    • You can go to production with passing tests and failing production.

    To solve the aforementioned issues, Spring Cloud Contract Verifier with Stub Runner was +created. The main idea is to give you very fast feedback, without the need to set up the +whole world of microservices. If you work on stubs, then the only applications you need +are those that your application directly uses.

    Stubbed Services

    Spring Cloud Contract Verifier gives you the certainty that the stubs that you use were +created by the service that you’re calling. Also, if you can use them, it means that they +were tested against the producer’s side. In short, you can trust those stubs.

    87.3 Purposes

    The main purposes of Spring Cloud Contract Verifier with Stub Runner are:

    • To ensure that WireMock/Messaging stubs (used when developing the client) do exactly +what the actual server-side implementation does.
    • To promote ATDD method and Microservices architectural style.
    • To provide a way to publish changes in contracts that are immediately visible on both +sides.
    • To generate boilerplate test code to be used on the server side.
    [Important]Important

    Spring Cloud Contract Verifier’s purpose is NOT to start writing business +features in the contracts. Assume that we have a business use case of fraud check. If a +user can be a fraud for 100 different reasons, we would assume that you would create 2 +contracts, one for the positive case and one for the negative case. Contract tests are +used to test contracts between applications and not to simulate full behavior.

    87.4 How It Works

    This section explores how Spring Cloud Contract Verifier with Stub Runner works.

    87.4.1 A Three-second Tour

    This very brief tour walks through using Spring Cloud Contract:

    You can find a somewhat longer tour +here.

    On the Producer Side

    To start working with Spring Cloud Contract, add files with REST/ messaging contracts +expressed in either Groovy DSL or YAML to the contracts directory, which is set by the +contractsDslDir property. By default, it is $rootDir/src/test/resources/contracts.

    Then add the Spring Cloud Contract Verifier dependency and plugin to your build file, as +shown in the following example:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>

    The following listing shows how to add the plugin, which should go in the build/plugins +portion of the file:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +</plugin>

    Running ./mvnw clean install automatically generates tests that verify the application +compliance with the added contracts. By default, the tests get generated under +org.springframework.cloud.contract.verifier.tests..

    As the implementation of the functionalities described by the contracts is not yet +present, the tests fail.

    To make them pass, you must add the correct implementation of either handling HTTP +requests or messages. Also, you must add a correct base test class for auto-generated +tests to the project. This class is extended by all the auto-generated tests, and it +should contain all the setup necessary to run them (for example RestAssuredMockMvc +controller setup or messaging test setup).

    Once the implementation and the test base class are in place, the tests pass, and both the +application and the stub artifacts are built and installed in the local Maven repository. +The changes can now be merged, and both the application and the stub artifacts may be +published in an online repository.

    On the Consumer Side

    Spring Cloud Contract Stub Runner can be used in the integration tests to get a running +WireMock instance or messaging route that simulates the actual service.

    To do so, add the dependency to Spring Cloud Contract Stub Runner, as shown in the +following example:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +	<scope>test</scope>
    +</dependency>

    You can get the Producer-side stubs installed in your Maven repository in either of two +ways:

    • By checking out the Producer side repository and adding contracts and generating the stubs +by running the following commands:

      $ cd local-http-server-repo
      +$ ./mvnw clean install -DskipTests
      [Tip]Tip

      The tests are being skipped because the Producer-side contract implementation is not +in place yet, so the automatically-generated contract tests fail.

    • By getting already-existing producer service stubs from a remote repository. To do so, +pass the stub artifact IDs and artifact repository URL as Spring Cloud Contract +Stub Runner properties, as shown in the following example:

      stubrunner:
      +  ids: 'com.example:http-server-dsl:+:stubs:8080'
      +  repositoryRoot: https://repo.spring.io/libs-snapshot

    Now you can annotate your test class with @AutoConfigureStubRunner. In the annotation, +provide the group-id and artifact-id values for Spring Cloud Contract Stub Runner to +run the collaborators' stubs for you, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment=WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
    +		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {
    [Tip]Tip

    Use the REMOTE stubsMode when downloading stubs from an online repository and +LOCAL for offline work.

    Now, in your integration test, you can receive stubbed versions of HTTP responses or +messages that are expected to be emitted by the collaborator service.

    87.4.2 A Three-minute Tour

    This brief tour walks through using Spring Cloud Contract:

    You can find an even more brief tour +here.

    On the Producer Side

    To start working with Spring Cloud Contract, add files with REST/ messaging contracts +expressed in either Groovy DSL or YAML to the contracts directory, which is set by the +contractsDslDir property. By default, it is $rootDir/src/test/resources/contracts.

    For the HTTP stubs, a contract defines what kind of response should be returned for a +given request (taking into account the HTTP methods, URLs, headers, status codes, and so +on). The following example shows how an HTTP stub contract in Groovy DSL:

    package contracts
    +
    +org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'PUT'
    +		url '/fraudcheck'
    +		body([
    +			   "client.id": $(regex('[0-9]{10}')),
    +			   loanAmount: 99999
    +		])
    +		headers {
    +			contentType('application/json')
    +		}
    +	}
    +	response {
    +		status OK()
    +		body([
    +			   fraudCheckStatus: "FRAUD",
    +			   "rejection.reason": "Amount too high"
    +		])
    +		headers {
    +			contentType('application/json')
    +		}
    +	}
    +}

    The same contract expressed in YAML would look like the following example:

    request:
    +  method: PUT
    +  url: /fraudcheck
    +  body:
    +    "client.id": 1234567890
    +    loanAmount: 99999
    +  headers:
    +    Content-Type: application/json
    +  matchers:
    +    body:
    +      - path: $.['client.id']
    +        type: by_regex
    +        value: "[0-9]{10}"
    +response:
    +  status: 200
    +  body:
    +    fraudCheckStatus: "FRAUD"
    +    "rejection.reason": "Amount too high"
    +  headers:
    +    Content-Type: application/json;charset=UTF-8

    In the case of messaging, you can define:

    • The input and the output messages can be defined (taking into account from and where it +was sent, the message body, and the header).
    • The methods that should be called after the message is received.
    • The methods that, when called, should trigger a message.

    The following example shows a Camel messaging contract expressed in Groovy DSL:

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					messageFrom('jms:delete')
    +					messageBody([
    +							bookName: 'foo'
    +					])
    +					messageHeaders {
    +						header('sample', 'header')
    +					}
    +					assertThat('bookWasDeleted()')
    +				}
    +			}

    The following example shows the same contract expressed in YAML:

    label: some_label
    +input:
    +  messageFrom: jms:delete
    +  messageBody:
    +    bookName: 'foo'
    +  messageHeaders:
    +    sample: header
    +  assertThat: bookWasDeleted()

    Then you can add Spring Cloud Contract Verifier dependency and plugin to your build file, +as shown in the following example:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>

    The following listing shows how to add the plugin, which should go in the build/plugins +portion of the file:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +</plugin>

    Running ./mvnw clean install automatically generates tests that verify the application +compliance with the added contracts. By default, the generated tests are under +org.springframework.cloud.contract.verifier.tests..

    The following example shows a sample auto-generated test for an HTTP contract:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +    // given:
    +        MockMvcRequestSpecification request = given()
    +                .header("Content-Type", "application/vnd.fraud.v1+json")
    +                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
    +
    +    // when:
    +        ResponseOptions response = given().spec(request)
    +                .put("/fraudcheck");
    +
    +    // then:
    +        assertThat(response.statusCode()).isEqualTo(200);
    +        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    +    // and:
    +        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
    +        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
    +}

    The preceding example uses Spring’s MockMvc to run the tests. This is the default test +mode for HTTP contracts. However, JAX-RS client and explicit HTTP invocations can also be +used. (To do so, change the testMode property of the plugin to JAX-RS or EXPLICIT, +respectively.)

    Since 2.1.0, it is also possible to use RestAssuredWebTestClient`with Spring’s reactive `WebTestClient +run under the hood. This is particularly recommended while working with Reactive, Web-Flux-based applications. +In order to use WebTestClient set testMode to WEBTESTCLIENT.

    Here is an example of a test generated in WEBTESTCLIENT test mode:

    [source,java,indent=0]
    @Test
    +	public void validate_shouldRejectABeerIfTooYoung() throws Exception {
    +		// given:
    +			WebTestClientRequestSpecification request = given()
    +					.header("Content-Type", "application/json")
    +					.body("{\"age\":10}");
    +
    +		// when:
    +			WebTestClientResponse response = given().spec(request)
    +					.post("/check");
    +
    +		// then:
    +			assertThat(response.statusCode()).isEqualTo(200);
    +			assertThat(response.header("Content-Type")).matches("application/json.*");
    +		// and:
    +			DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +			assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK");
    +	}

    Apart from the default JUnit 4, you can instead use JUnit 5 or Spock tests, by setting the plugin +testFramework property to either JUNIT5 or Spock.

    [Tip]Tip

    You can now also generate WireMock scenarios based on the contracts, by including an +order number followed by an underscore at the beginning of the contract file names.

    The following example shows an auto-generated test in Spock for a messaging stub contract:

    [source,groovy,indent=0]
    given:
    +	 ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +		\'\'\'{"bookName":"foo"}\'\'\',
    +		['sample': 'header']
    +	)
    +
    +when:
    +	 contractVerifierMessaging.send(inputMessage, 'jms:delete')
    +
    +then:
    +	 noExceptionThrown()
    +	 bookWasDeleted()

    As the implementation of the functionalities described by the contracts is not yet +present, the tests fail.

    To make them pass, you must add the correct implementation of handling either HTTP +requests or messages. Also, you must add a correct base test class for auto-generated +tests to the project. This class is extended by all the auto-generated tests and should +contain all the setup necessary to run them (for example, RestAssuredMockMvc controller +setup or messaging test setup).

    Once the implementation and the test base class are in place, the tests pass, and both the +application and the stub artifacts are built and installed in the local Maven repository. +Information about installing the stubs jar to the local repository appears in the logs, as +shown in the following example:

    [INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
    +[INFO]
    +[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
    +[INFO]
    +[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
    +[INFO]
    +[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
    +[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

    You can now merge the changes and publish both the application and the stub artifacts +in an online repository.

    Docker Project

    In order to enable working with contracts while creating applications in non-JVM +technologies, the springcloud/spring-cloud-contract Docker image has been created. It +contains a project that automatically generates tests for HTTP contracts and executes them +in EXPLICIT test mode. Then, if the tests pass, it generates Wiremock stubs and, +optionally, publishes them to an artifact manager. In order to use the image, you can +mount the contracts into the /contracts directory and set a few environment variables.

    On the Consumer Side

    Spring Cloud Contract Stub Runner can be used in the integration tests to get a running +WireMock instance or messaging route that simulates the actual service.

    To get started, add the dependency to Spring Cloud Contract Stub Runner:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +	<scope>test</scope>
    +</dependency>

    You can get the Producer-side stubs installed in your Maven repository in either of two +ways:

    • By checking out the Producer side repository and adding contracts and generating the +stubs by running the following commands:

      $ cd local-http-server-repo
      +$ ./mvnw clean install -DskipTests
      [Note]Note

      The tests are skipped because the Producer-side contract implementation is not yet +in place, so the automatically-generated contract tests fail.

    • Getting already existing producer service stubs from a remote repository. To do so, +pass the stub artifact IDs and artifact repository URl as Spring Cloud Contract Stub +Runner properties, as shown in the following example:

      stubrunner:
      +  ids: 'com.example:http-server-dsl:+:stubs:8080'
      +  repositoryRoot: https://repo.spring.io/libs-snapshot

    Now you can annotate your test class with @AutoConfigureStubRunner. In the annotation, +provide the group-id and artifact-id for Spring Cloud Contract Stub Runner to run +the collaborators' stubs for you, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment=WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
    +		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {
    [Tip]Tip

    Use the REMOTE stubsMode when downloading stubs from an online repository and +LOCAL for offline work.

    In your integration test, you can receive stubbed versions of HTTP responses or messages +that are expected to be emitted by the collaborator service. You can see entries similar +to the following in the build logs:

    2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
    +2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
    +2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
    +2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
    +2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
    +2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
    +2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]

    87.4.3 Defining the Contract

    As consumers of services, we need to define what exactly we want to achieve. We need to +formulate our expectations. That is why we write contracts.

    Assume that you want to send a request containing the ID of a client company and the +amount it wants to borrow from us. You also want to send it to the /fraudcheck url via +the PUT method.

    Groovy DSL.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package contracts
    +
    +org.springframework.cloud.contract.spec.Contract.make {
    +	request { // (1)
    +		method 'PUT' // (2)
    +		url '/fraudcheck' // (3)
    +		body([ // (4)
    +			   "client.id": $(regex('[0-9]{10}')),
    +			   loanAmount : 99999
    +		])
    +		headers { // (5)
    +			contentType('application/json')
    +		}
    +	}
    +	response { // (6)
    +		status OK() // (7)
    +		body([ // (8)
    +			   fraudCheckStatus  : "FRAUD",
    +			   "rejection.reason": "Amount too high"
    +		])
    +		headers { // (9)
    +			contentType('application/json')
    +		}
    +	}
    +}
    +
    +/*
    +From the Consumer perspective, when shooting a request in the integration test:
    +
    +(1) - If the consumer sends a request
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the response will be sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` equal to `application/json`
    +
    +From the Producer perspective, in the autogenerated producer-side test:
    +
    +(1) - A request will be sent to the producer
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the test will assert if the response has been sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` matching `application/json.*`
    + */

    +

    YAML.  +

    request: # (1)
    +  method: PUT # (2)
    +  url: /fraudcheck # (3)
    +  body: # (4)
    +    "client.id": 1234567890
    +    loanAmount: 99999
    +  headers: # (5)
    +    Content-Type: application/json
    +  matchers:
    +    body:
    +      - path: $.['client.id'] # (6)
    +        type: by_regex
    +        value: "[0-9]{10}"
    +response: # (7)
    +  status: 200 # (8)
    +  body:  # (9)
    +    fraudCheckStatus: "FRAUD"
    +    "rejection.reason": "Amount too high"
    +  headers: # (10)
    +    Content-Type: application/json;charset=UTF-8
    +
    +
    +#From the Consumer perspective, when shooting a request in the integration test:
    +#
    +#(1) - If the consumer sends a request
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
    +#(7) - then the response will be sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json`
    +#
    +#From the Producer perspective, in the autogenerated producer-side test:
    +#
    +#(1) - A request will be sent to the producer
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id` `1234567890`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(7) - then the test will assert if the response has been sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8`

    +

    87.4.4 Client Side

    Spring Cloud Contract generates stubs, which you can use during client-side testing. +You get a running WireMock instance/Messaging route that simulates the service. +You would like to feed that instance with a proper stub definition.

    At some point in time, you need to send a request to the Fraud Detection service.

    ResponseEntity<FraudServiceResponse> response = restTemplate.exchange(
    +		"http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
    +		new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class);

    Annotate your test class with @AutoConfigureStubRunner. In the annotation provide the group id and artifact id for the Stub Runner to download stubs of your collaborators.

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {
    +		"com.example:http-server-dsl:+:stubs:6565" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {

    After that, during the tests, Spring Cloud Contract automatically finds the stubs +(simulating the real service) in the Maven repository and exposes them on a configured +(or random) port.

    87.4.5 Server Side

    Since you are developing your stub, you need to be sure that it actually resembles your +concrete implementation. You cannot have a situation where your stub acts in one way and +your application behaves in a different way, especially in production.

    To ensure that your application behaves the way you define in your stub, tests are +generated from the stub you provide.

    The autogenerated test looks, more or less, like this:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +    // given:
    +        MockMvcRequestSpecification request = given()
    +                .header("Content-Type", "application/vnd.fraud.v1+json")
    +                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
    +
    +    // when:
    +        ResponseOptions response = given().spec(request)
    +                .put("/fraudcheck");
    +
    +    // then:
    +        assertThat(response.statusCode()).isEqualTo(200);
    +        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    +    // and:
    +        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
    +        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
    +}

    87.5 Step-by-step Guide to Consumer Driven Contracts (CDC)

    Consider an example of Fraud Detection and the Loan Issuance process. The business +scenario is such that we want to issue loans to people but do not want them to steal from +us. The current implementation of our system grants loans to everybody.

    Assume that Loan Issuance is a client to the Fraud Detection server. In the current +sprint, we must develop a new feature: if a client wants to borrow too much money, then +we mark the client as a fraud.

    Technical remark - Fraud Detection has an artifact-id of http-server, while Loan +Issuance has an artifact-id of http-client, and both have a group-id of com.example.

    Social remark - both client and server development teams need to communicate directly and +discuss changes while going through the process. CDC is all about communication.

    The server +side code is available here and the +client code here.

    [Tip]Tip

    In this case, the producer owns the contracts. Physically, all the contract are +in the producer’s repository.

    87.5.1 Technical note

    If using the SNAPSHOT / Milestone / Release Candidate versions please add the +following section to your build:

    Maven.  +

    <repositories>
    +	<repository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +</repositories>
    +<pluginRepositories>
    +	<pluginRepository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +</pluginRepositories>

    +

    Gradle.  +

    repositories {
    +	mavenCentral()
    +	mavenLocal()
    +	maven { url "https://repo.spring.io/snapshot" }
    +	maven { url "https://repo.spring.io/milestone" }
    +	maven { url "https://repo.spring.io/release" }
    +}

    +

    87.5.2 Consumer side (Loan Issuance)

    As a developer of the Loan Issuance service (a consumer of the Fraud Detection server), you might do the following steps:

    1. Start doing TDD by writing a test for your feature.
    2. Write the missing implementation.
    3. Clone the Fraud Detection service repository locally.
    4. Define the contract locally in the repo of Fraud Detection service.
    5. Add the Spring Cloud Contract Verifier plugin.
    6. Run the integration tests.
    7. File a pull request.
    8. Create an initial implementation.
    9. Take over the pull request.
    10. Write the missing implementation.
    11. Deploy your app.
    12. Work online.

    Start doing TDD by writing a test for your feature.

    @Test
    +public void shouldBeRejectedDueToAbnormalLoanAmount() {
    +	// given:
    +	LoanApplication application = new LoanApplication(new Client("1234567890"),
    +			99999);
    +	// when:
    +	LoanApplicationResult loanApplication = service.loanApplication(application);
    +	// then:
    +	assertThat(loanApplication.getLoanApplicationStatus())
    +			.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
    +	assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
    +}

    Assume that you have written a test of your new feature. If a loan application for a big +amount is received, the system should reject that loan application with some description.

    Write the missing implementation.

    At some point in time, you need to send a request to the Fraud Detection service. Assume +that you need to send the request containing the ID of the client and the amount the +client wants to borrow. You want to send it to the /fraudcheck url via the PUT method.

    ResponseEntity<FraudServiceResponse> response = restTemplate.exchange(
    +		"http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
    +		new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class);

    For simplicity, the port of the Fraud Detection service is set to 8080, and the +application runs on 8090.

    If you start the test at this point, it breaks, because no service currently runs on port +8080.

    Clone the Fraud Detection service repository locally.

    You can start by playing around with the server side contract. To do so, you must first +clone it.

    $ git clone https://your-git-server.com/server-side.git local-http-server-repo

    Define the contract locally in the repo of Fraud Detection service.

    As a consumer, you need to define what exactly you want to achieve. You need to formulate +your expectations. To do so, write the following contract:

    [Important]Important

    Place the contract under src/test/resources/contracts/fraud folder. The fraud folder +is important because the producer’s test base class name references that folder.

    Groovy DSL.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package contracts
    +
    +org.springframework.cloud.contract.spec.Contract.make {
    +	request { // (1)
    +		method 'PUT' // (2)
    +		url '/fraudcheck' // (3)
    +		body([ // (4)
    +			   "client.id": $(regex('[0-9]{10}')),
    +			   loanAmount : 99999
    +		])
    +		headers { // (5)
    +			contentType('application/json')
    +		}
    +	}
    +	response { // (6)
    +		status OK() // (7)
    +		body([ // (8)
    +			   fraudCheckStatus  : "FRAUD",
    +			   "rejection.reason": "Amount too high"
    +		])
    +		headers { // (9)
    +			contentType('application/json')
    +		}
    +	}
    +}
    +
    +/*
    +From the Consumer perspective, when shooting a request in the integration test:
    +
    +(1) - If the consumer sends a request
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the response will be sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` equal to `application/json`
    +
    +From the Producer perspective, in the autogenerated producer-side test:
    +
    +(1) - A request will be sent to the producer
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the test will assert if the response has been sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` matching `application/json.*`
    + */

    +

    YAML.  +

    request: # (1)
    +  method: PUT # (2)
    +  url: /fraudcheck # (3)
    +  body: # (4)
    +    "client.id": 1234567890
    +    loanAmount: 99999
    +  headers: # (5)
    +    Content-Type: application/json
    +  matchers:
    +    body:
    +      - path: $.['client.id'] # (6)
    +        type: by_regex
    +        value: "[0-9]{10}"
    +response: # (7)
    +  status: 200 # (8)
    +  body:  # (9)
    +    fraudCheckStatus: "FRAUD"
    +    "rejection.reason": "Amount too high"
    +  headers: # (10)
    +    Content-Type: application/json;charset=UTF-8
    +
    +
    +#From the Consumer perspective, when shooting a request in the integration test:
    +#
    +#(1) - If the consumer sends a request
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
    +#(7) - then the response will be sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json`
    +#
    +#From the Producer perspective, in the autogenerated producer-side test:
    +#
    +#(1) - A request will be sent to the producer
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id` `1234567890`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(7) - then the test will assert if the response has been sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8`

    +

    The YML contract is quite straight-forward. However when you take a look at the Contract +written using a statically typed Groovy DSL - you might wonder what the +value(client(…​), server(…​)) parts are. By using this notation, Spring Cloud +Contract lets you define parts of a JSON block, a URL, etc., which are dynamic. In case +of an identifier or a timestamp, you need not hardcode a value. You want to allow some +different ranges of values. To enable ranges of values, you can set regular expressions +matching those values for the consumer side. You can provide the body by means of either +a map notation or String with interpolations. +Consult the Chapter 94, Contract DSL section for more information. We highly recommend using the map notation!

    [Tip]Tip

    You must understand the map notation in order to set up contracts. Please read the +Groovy docs regarding JSON.

    The previously shown contract is an agreement between two sides that:

    • if an HTTP request is sent with all of

      • a PUT method on the /fraudcheck endpoint,
      • a JSON body with a client.id that matches the regular expression [0-9]{10} and +loanAmount equal to 99999,
      • and a Content-Type header with a value of application/vnd.fraud.v1+json,
    • then an HTTP response is sent to the consumer that

      • has status 200,
      • contains a JSON body with the fraudCheckStatus field containing a value FRAUD and +the rejectionReason field having value Amount too high,
      • and a Content-Type header with a value of application/vnd.fraud.v1+json.

    Once you are ready to check the API in practice in the integration tests, you need to +install the stubs locally.

    Add the Spring Cloud Contract Verifier plugin.

    We can add either a Maven or a Gradle plugin. In this example, you see how to add Maven. +First, add the Spring Cloud Contract BOM.

    <dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>${spring-cloud-release.version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    Next, add the Spring Cloud Contract Verifier Maven plugin

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +		<convertToYaml>true</convertToYaml>
    +	</configuration>
    +</plugin>

    Since the plugin was added, you get the Spring Cloud Contract Verifier features which, +from the provided contracts:

    • generate and run tests
    • produce and install stubs

    You do not want to generate tests since you, as the consumer, want only to play with the +stubs. You need to skip the test generation and execution. When you execute:

    $ cd local-http-server-repo
    +$ ./mvnw clean install -DskipTests

    In the logs, you see something like this:

    [INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
    +[INFO]
    +[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
    +[INFO]
    +[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
    +[INFO]
    +[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
    +[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

    The following line is extremely important:

    [INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

    It confirms that the stubs of the http-server have been installed in the local +repository.

    Run the integration tests.

    In order to profit from the Spring Cloud Contract Stub Runner functionality of automatic +stub downloading, you must do the following in your consumer side project (Loan +Application service):

    Add the Spring Cloud Contract BOM:

    <dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>${spring-cloud-release-train.version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    Add the dependency to Spring Cloud Contract Stub Runner:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +	<scope>test</scope>
    +</dependency>

    Annotate your test class with @AutoConfigureStubRunner. In the annotation, provide the +group-id and artifact-id for the Stub Runner to download the stubs of your +collaborators. (Optional step) Because you’re playing with the collaborators offline, you +can also provide the offline work switch (StubRunnerProperties.StubsMode.LOCAL).

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {
    +		"com.example:http-server-dsl:+:stubs:6565" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {

    Now, when you run your tests, you see something like this:

    2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
    +2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
    +2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
    +2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
    +2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
    +2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
    +2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]

    This output means that Stub Runner has found your stubs and started a server for your app +with group id com.example, artifact id http-server with version 0.0.1-SNAPSHOT of +the stubs and with stubs classifier on port 8080.

    File a pull request.

    What you have done until now is an iterative process. You can play around with the +contract, install it locally, and work on the consumer side until the contract works as +you wish.

    Once you are satisfied with the results and the test passes, publish a pull request to +the server side. Currently, the consumer side work is done.

    87.5.3 Producer side (Fraud Detection server)

    As a developer of the Fraud Detection server (a server to the Loan Issuance service):

    Create an initial implementation.

    As a reminder, you can see the initial implementation here:

    @RequestMapping(value = "/fraudcheck", method = PUT)
    +public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
    +return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
    +}

    Take over the pull request.

    $ git checkout -b contract-change-pr master
    +$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr

    You must add the dependencies needed by the autogenerated tests:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>

    In the configuration of the Maven plugin, pass the packageWithBaseClasses property

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +		<convertToYaml>true</convertToYaml>
    +	</configuration>
    +</plugin>
    [Important]Important

    This example uses "convention based" naming by setting the +packageWithBaseClasses property. Doing so means that the two last packages combine to +make the name of the base test class. In our case, the contracts were placed under +src/test/resources/contracts/fraud. Since you do not have two packages starting from +the contracts folder, pick only one, which should be fraud. Add the Base suffix and +capitalize fraud. That gives you the FraudBase test class name.

    All the generated tests extend that class. Over there, you can set up your Spring Context +or whatever is necessary. In this case, use Rest Assured MVC to +start the server side FraudDetectionController.

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package com.example.fraud;
    +
    +import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +
    +public class FraudBase {
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
    +				new FraudStatsController(stubbedStatsProvider()));
    +	}
    +
    +	private StatsProvider stubbedStatsProvider() {
    +		return fraudType -> {
    +			switch (fraudType) {
    +			case DRUNKS:
    +				return 100;
    +			case ALL:
    +				return 200;
    +			}
    +			return 0;
    +		};
    +	}
    +
    +	public void assertThatRejectionReasonIsNull(Object rejectionReason) {
    +		assert rejectionReason == null;
    +	}
    +
    +}

    Now, if you run the ./mvnw clean install, you get something like this:

    Results :
    +
    +Tests in error:
    +  ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...

    This error occurs because you have a new contract from which a test was generated and it +failed since you have not implemented the feature. The auto-generated test would look +like this:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +    // given:
    +        MockMvcRequestSpecification request = given()
    +                .header("Content-Type", "application/vnd.fraud.v1+json")
    +                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
    +
    +    // when:
    +        ResponseOptions response = given().spec(request)
    +                .put("/fraudcheck");
    +
    +    // then:
    +        assertThat(response.statusCode()).isEqualTo(200);
    +        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    +    // and:
    +        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
    +        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
    +}

    If you used the Groovy DSL, you can see, all the producer() parts of the Contract that were present in the +value(consumer(…​), producer(…​)) blocks got injected into the test. +In case of using YAML, the same applied for the matchers sections of the response.

    Note that, on the producer side, you are also doing TDD. The expectations are expressed +in the form of a test. This test sends a request to our own application with the URL, +headers, and body defined in the contract. It also is expecting precisely defined values +in the response. In other words, you have the red part of red, green, and +refactor. It is time to convert the red into the green.

    Write the missing implementation.

    Because you know the expected input and expected output, you can write the missing +implementation:

    @RequestMapping(value = "/fraudcheck", method = PUT)
    +public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
    +if (amountGreaterThanThreshold(fraudCheck)) {
    +	return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
    +}
    +return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
    +}

    When you execute ./mvnw clean install again, the tests pass. Since the Spring Cloud +Contract Verifier plugin adds the tests to the generated-test-sources, you can +actually run those tests from your IDE.

    Deploy your app.

    Once you finish your work, you can deploy your change. First, merge the branch:

    $ git checkout master
    +$ git merge --no-ff contract-change-pr
    +$ git push origin master

    Your CI might run something like ./mvnw clean deploy, which would publish both the +application and the stub artifacts.

    87.5.4 Consumer Side (Loan Issuance) Final Step

    As a developer of the Loan Issuance service (a consumer of the Fraud Detection server):

    Merge branch to master.

    $ git checkout master
    +$ git merge --no-ff contract-change-pr

    Work online.

    Now you can disable the offline work for Spring Cloud Contract Stub Runner and indicate +where the repository with your stubs is located. At this moment the stubs of the server +side are automatically downloaded from Nexus/Artifactory. You can set the value of +stubsMode to REMOTE. The following code shows an example of +achieving the same thing by changing the properties.

    stubrunner:
    +  ids: 'com.example:http-server-dsl:+:stubs:8080'
    +  repositoryRoot: https://repo.spring.io/libs-snapshot

    That’s it!

    87.6 Dependencies

    The best way to add dependencies is to use the proper starter dependency.

    For stub-runner, use spring-cloud-starter-stub-runner. When you use a plugin, add +spring-cloud-starter-contract-verifier.

    87.7 Additional Links

    Here are some resources related to Spring Cloud Contract Verifier and Stub Runner. Note +that some may be outdated, because the Spring Cloud Contract Verifier project is under +constant development.

    87.7.1 Spring Cloud Contract video

    You can check out the video from the Warsaw JUG about Spring Cloud Contract:

    87.8 Samples

    You can find some samples at +samples.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_messaging.html b/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_messaging.html new file mode 100644 index 00000000..ce00434b --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_messaging.html @@ -0,0 +1,260 @@ + + + 91. Spring Cloud Contract Verifier Messaging

    91. Spring Cloud Contract Verifier Messaging

    Spring Cloud Contract Verifier lets you verify applications that use messaging as a +means of communication. All of the integrations shown in this document work with Spring, +but you can also create one of your own and use that.

    91.1 Integrations

    You can use one of the following four integration configurations:

    • Apache Camel
    • Spring Integration
    • Spring Cloud Stream
    • Spring AMQP

    Since we use Spring Boot, if you have added one of these libraries to the classpath, all +the messaging configuration is automatically set up.

    [Important]Important

    Remember to put @AutoConfigureMessageVerifier on the base class of your +generated tests. Otherwise, messaging part of Spring Cloud Contract Verifier does not +work.

    [Important]Important

    If you want to use Spring Cloud Stream, remember to add a dependency on +org.springframework.cloud:spring-cloud-stream-test-support, as shown here:

    Maven.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-stream-test-support</artifactId>
    +    <scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile "org.springframework.cloud:spring-cloud-stream-test-support"

    +

    91.2 Manual Integration Testing

    The main interface used by the tests is +org.springframework.cloud.contract.verifier.messaging.MessageVerifier. +It defines how to send and receive messages. You can create your own implementation to +achieve the same goal.

    In a test, you can inject a ContractVerifierMessageExchange to send and receive +messages that follow the contract. Then add @AutoConfigureMessageVerifier to your test. +Here’s an example:

    @RunWith(SpringTestRunner.class)
    +@SpringBootTest
    +@AutoConfigureMessageVerifier
    +public static class MessagingContractTests {
    +
    +  @Autowired
    +  private MessageVerifier verifier;
    +  ...
    +}
    [Note]Note

    If your tests require stubs as well, then @AutoConfigureStubRunner includes the +messaging configuration, so you only need the one annotation.

    91.3 Publisher-Side Test Generation

    Having the input or outputMessage sections in your DSL results in creation of tests +on the publisher’s side. By default, JUnit 4 tests are created. However, there is also a +possibility to create JUnit 5 or Spock tests.

    There are 3 main scenarios that we should take into consideration:

    • Scenario 1: There is no input message that produces an output message. The output +message is triggered by a component inside the application (for example, scheduler).
    • Scenario 2: The input message triggers an output message.
    • Scenario 3: The input message is consumed and there is no output message.
    [Important]Important

    The destination passed to messageFrom or sentTo can have different +meanings for different messaging implementations. For Stream and Integration it is +first resolved as a destination of a channel. Then, if there is no such destination +it is resolved as a channel name. For Camel, that’s a certain component (for example, +jms).

    91.3.1 Scenario 1: No Input Message

    For the given contract:

    Groovy DSL.  +

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					triggeredBy('bookReturnedTriggered()')
    +				}
    +				outputMessage {
    +					sentTo('activemq:output')
    +					body('''{ "bookName" : "foo" }''')
    +					headers {
    +						header('BOOK-NAME', 'foo')
    +						messagingContentType(applicationJson())
    +					}
    +				}
    +			}

    +

    YAML.  +

    label: some_label
    +input:
    +  triggeredBy: bookReturnedTriggered
    +outputMessage:
    +  sentTo: activemq:output
    +  body:
    +    bookName: foo
    +  headers:
    +    BOOK-NAME: foo
    +    contentType: application/json

    +

    The following JUnit test is created:

    					'''
    + // when:
    +  bookReturnedTriggered();
    +
    + // then:
    +  ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output");
    +  assertThat(response).isNotNull();
    +  assertThat(response.getHeader("BOOK-NAME")).isNotNull();
    +  assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
    +  assertThat(response.getHeader("contentType")).isNotNull();
    +  assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");
    + // and:
    +  DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
    +  assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
    +'''

    And the following Spock test would be created:

    					'''
    + when:
    +  bookReturnedTriggered()
    +
    + then:
    +  ContractVerifierMessage response = contractVerifierMessaging.receive('activemq:output')
    +  assert response != null
    +  response.getHeader('BOOK-NAME')?.toString()  == 'foo'
    +  response.getHeader('contentType')?.toString()  == 'application/json'
    + and:
    +  DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
    +  assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
    +
    +'''

    91.3.2 Scenario 2: Output Triggered by Input

    For the given contract:

    Groovy DSL.  +

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					messageFrom('jms:input')
    +					messageBody([
    +							bookName: 'foo'
    +					])
    +					messageHeaders {
    +						header('sample', 'header')
    +					}
    +				}
    +				outputMessage {
    +					sentTo('jms:output')
    +					body([
    +							bookName: 'foo'
    +					])
    +					headers {
    +						header('BOOK-NAME', 'foo')
    +					}
    +				}
    +			}

    +

    YAML.  +

    label: some_label
    +input:
    +  messageFrom: jms:input
    +  messageBody:
    +    bookName: 'foo'
    +  messageHeaders:
    +    sample: header
    +outputMessage:
    +  sentTo: jms:output
    +  body:
    +    bookName: foo
    +  headers:
    +    BOOK-NAME: foo

    +

    The following JUnit test is created:

    					'''
    +// given:
    + ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +  "{\\"bookName\\":\\"foo\\"}"
    +, headers()
    +  .header("sample", "header"));
    +
    +// when:
    + contractVerifierMessaging.send(inputMessage, "jms:input");
    +
    +// then:
    + ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
    + assertThat(response).isNotNull();
    + assertThat(response.getHeader("BOOK-NAME")).isNotNull();
    + assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
    +// and:
    + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
    + assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
    +'''

    And the following Spock test would be created:

    					"""\
    +given:
    +   ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +    '''{"bookName":"foo"}''',
    +    ['sample': 'header']
    +  )
    +
    +when:
    +   contractVerifierMessaging.send(inputMessage, 'jms:input')
    +
    +then:
    +   ContractVerifierMessage response = contractVerifierMessaging.receive('jms:output')
    +   assert response !- null
    +   response.getHeader('BOOK-NAME')?.toString()  == 'foo'
    +and:
    +   DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
    +   assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
    +"""

    91.3.3 Scenario 3: No Output Message

    For the given contract:

    Groovy DSL.  +

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					messageFrom('jms:delete')
    +					messageBody([
    +							bookName: 'foo'
    +					])
    +					messageHeaders {
    +						header('sample', 'header')
    +					}
    +					assertThat('bookWasDeleted()')
    +				}
    +			}

    +

    YAML.  +

    label: some_label
    +input:
    +  messageFrom: jms:delete
    +  messageBody:
    +    bookName: 'foo'
    +  messageHeaders:
    +    sample: header
    +  assertThat: bookWasDeleted()

    +

    The following JUnit test is created:

    					'''
    +// given:
    + ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +	"{\\"bookName\\":\\"foo\\"}"
    +, headers()
    +	.header("sample", "header"));
    +
    +// when:
    + contractVerifierMessaging.send(inputMessage, "jms:delete");
    +
    +// then:
    + bookWasDeleted();
    +'''

    And the following Spock test would be created:

    					'''
    +given:
    +	 ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +		\'\'\'{"bookName":"foo"}\'\'\',
    +		['sample': 'header']
    +	)
    +
    +when:
    +	 contractVerifierMessaging.send(inputMessage, 'jms:delete')
    +
    +then:
    +	 noExceptionThrown()
    +	 bookWasDeleted()
    +'''

    91.4 Consumer Stub Generation

    Unlike the HTTP part, in messaging, we need to publish the Groovy DSL inside the JAR with +a stub. Then it is parsed on the consumer side and proper stubbed routes are created.

    For more information, see Chapter 93, Stub Runner for Messaging section.

    Maven.  +

    <dependencies>
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    +	</dependency>
    +
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +		<scope>test</scope>
    +	</dependency>
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-stream-test-support</artifactId>
    +		<scope>test</scope>
    +	</dependency>
    +</dependencies>
    +
    +<dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>Greenwich.BUILD-SNAPSHOT</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    +

    Gradle.  +

    ext {
    +	contractsDir = file("mappings")
    +	stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
    +}
    +
    +// Automatically added by plugin:
    +// copyContracts - copies contracts to the output folder from which JAR will be created
    +// verifierStubsJar - JAR with a provided stub suffix
    +// the presented publication is also added by the plugin but you can modify it as you wish
    +
    +publishing {
    +	publications {
    +		stubs(MavenPublication) {
    +			artifactId "${project.name}-stubs"
    +			artifact verifierStubsJar
    +		}
    +	}
    +}

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_setup.html b/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_setup.html new file mode 100644 index 00000000..2e82a902 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract_verifier_setup.html @@ -0,0 +1,7 @@ + + + 89. Spring Cloud Contract Verifier Setup

    89. Spring Cloud Contract Verifier Setup

    You can set up Spring Cloud Contract Verifier in the following ways:

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_contract_wiremock.html b/Greenwich.SR5/multi/multi__spring_cloud_contract_wiremock.html new file mode 100644 index 00000000..570b35e8 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_contract_wiremock.html @@ -0,0 +1,322 @@ + + + 97. Spring Cloud Contract WireMock

    97. Spring Cloud Contract WireMock

    The Spring Cloud Contract WireMock modules let you use WireMock in a +Spring Boot application. Check out the +samples +for more details.

    If you have a Spring Boot application that uses Tomcat as an embedded server (which is +the default with spring-boot-starter-web), you can add +spring-cloud-starter-contract-stub-runner to your classpath and add @AutoConfigureWireMock in +order to be able to use Wiremock in your tests. Wiremock runs as a stub server and you +can register stub behavior using a Java API or via static JSON declarations as part of +your test. The following code shows an example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
    +@AutoConfigureWireMock(port = 0)
    +public class WiremockForDocsTests {
    +
    +	// A service that calls out over HTTP
    +	@Autowired
    +	private Service service;
    +
    +	@Before
    +	public void setup() {
    +		this.service.setBase("http://localhost:"
    +				+ this.environment.getProperty("wiremock.server.port"));
    +	}
    +
    +	// Using the WireMock APIs in the normal way:
    +	@Test
    +	public void contextLoads() throws Exception {
    +		// Stubbing WireMock
    +		stubFor(get(urlEqualTo("/resource")).willReturn(aResponse()
    +				.withHeader("Content-Type", "text/plain").withBody("Hello World!")));
    +		// We're asserting if WireMock responded properly
    +		assertThat(this.service.go()).isEqualTo("Hello World!");
    +	}
    +
    +}

    To start the stub server on a different port use (for example), +@AutoConfigureWireMock(port=9999). For a random port, use a value of 0. The stub +server port can be bound in the test application context with the "wiremock.server.port" +property. Using @AutoConfigureWireMock adds a bean of type WiremockConfiguration to +your test application context, where it will be cached in between methods and classes +having the same context, the same as for Spring integration tests. Also you can inject a bean of type WireMockServer into your test.

    97.1 Registering Stubs Automatically

    If you use @AutoConfigureWireMock, it registers WireMock JSON stubs from the file +system or classpath (by default, from file:src/test/resources/mappings). You can +customize the locations using the stubs attribute in the annotation, which can be an +Ant-style resource pattern or a directory. In the case of a directory, */.json is +appended. The following code shows an example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureWireMock(stubs="classpath:/stubs")
    +public class WiremockImportApplicationTests {
    +
    +	@Autowired
    +	private Service service;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		assertThat(this.service.go()).isEqualTo("Hello World!");
    +	}
    +
    +}
    [Note]Note

    Actually, WireMock always loads mappings from src/test/resources/mappings as +well as the custom locations in the stubs attribute. To change this behavior, you can +also specify a files root as described in the next section of this document.

    If you’re using Spring Cloud Contract’s default stub jars, then your +stubs are stored under /META-INF/group-id/artifact-id/versions/mappings/ folder. If you want to register all stubs from that location, from all embedded JARs, then it’s enough to use the following syntax.

    @AutoConfigureWireMock(port = 0, stubs = "classpath*:/META-INF/**/mappings/**/*.json")

    97.2 Using Files to Specify the Stub Bodies

    WireMock can read response bodies from files on the classpath or the file system. In that +case, you can see in the JSON DSL that the response has a bodyFileName instead of a +(literal) body. The files are resolved relative to a root directory (by default, +src/test/resources/__files). To customize this location you can set the files +attribute in the @AutoConfigureWireMock annotation to the location of the parent +directory (in other words, __files is a subdirectory). You can use Spring resource +notation to refer to file:…​ or classpath:…​ locations. Generic URLs are not +supported. A list of values can be given, in which case WireMock resolves the first file +that exists when it needs to find a response body.

    [Note]Note

    When you configure the files root, it also affects the +automatic loading of stubs, because they come from the root location +in a subdirectory called "mappings". The value of files has no +effect on the stubs loaded explicitly from the stubs attribute.

    97.3 Alternative: Using JUnit Rules

    For a more conventional WireMock experience, you can use JUnit @Rules to start and stop +the server. To do so, use the WireMockSpring convenience class to obtain an Options +instance, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
    +public class WiremockForDocsClassRuleTests {
    +
    +	// Start WireMock on some dynamic port
    +	// for some reason `dynamicPort()` is not working properly
    +	@ClassRule
    +	public static WireMockClassRule wiremock = new WireMockClassRule(
    +			WireMockSpring.options().dynamicPort());
    +
    +	// A service that calls out over HTTP to wiremock's port
    +	@Autowired
    +	private Service service;
    +
    +	@Before
    +	public void setup() {
    +		this.service.setBase("http://localhost:" + wiremock.port());
    +	}
    +
    +	// Using the WireMock APIs in the normal way:
    +	@Test
    +	public void contextLoads() throws Exception {
    +		// Stubbing WireMock
    +		wiremock.stubFor(get(urlEqualTo("/resource")).willReturn(aResponse()
    +				.withHeader("Content-Type", "text/plain").withBody("Hello World!")));
    +		// We're asserting if WireMock responded properly
    +		assertThat(this.service.go()).isEqualTo("Hello World!");
    +	}
    +
    +}

    The @ClassRule means that the server shuts down after all the methods in this class +have been run.

    97.4 Relaxed SSL Validation for Rest Template

    WireMock lets you stub a "secure" server with an "https" URL protocol. If your +application wants to contact that stub server in an integration test, it will find that +the SSL certificates are not valid (the usual problem with self-installed certificates). +The best option is often to re-configure the client to use "http". If that’s not an +option, you can ask Spring to configure an HTTP client that ignores SSL validation errors +(do so only for tests, of course).

    To make this work with minimum fuss, you need to be using the Spring Boot +RestTemplateBuilder in your app, as shown in the following example:

    @Bean
    +public RestTemplate restTemplate(RestTemplateBuilder builder) {
    +	return builder.build();
    +}

    You need RestTemplateBuilder because the builder is passed through callbacks to +initialize it, so the SSL validation can be set up in the client at that point. This +happens automatically in your test if you are using the @AutoConfigureWireMock +annotation or the stub runner. If you use the JUnit @Rule approach, you need to add the +@AutoConfigureHttpClient annotation as well, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest("app.baseUrl=https://localhost:6443")
    +@AutoConfigureHttpClient
    +public class WiremockHttpsServerApplicationTests {
    +
    +	@ClassRule
    +	public static WireMockClassRule wiremock = new WireMockClassRule(
    +			WireMockSpring.options().httpsPort(6443));
    +...
    +}

    If you are using spring-boot-starter-test, you have the Apache HTTP client on the +classpath and it is selected by the RestTemplateBuilder and configured to ignore SSL +errors. If you use the default java.net client, you do not need the annotation (but it +won’t do any harm). There is no support currently for other clients, but it may be added +in future releases.

    To disable the custom RestTemplateBuilder, set the wiremock.rest-template-ssl-enabled +property to false.

    97.5 WireMock and Spring MVC Mocks

    Spring Cloud Contract provides a convenience class that can load JSON WireMock stubs into +a Spring MockRestServiceServer. The following code shows an example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.NONE)
    +public class WiremockForDocsMockServerApplicationTests {
    +
    +	@Autowired
    +	private RestTemplate restTemplate;
    +
    +	@Autowired
    +	private Service service;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		// will read stubs classpath
    +		MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate)
    +				.baseUrl("https://example.org").stubs("classpath:/stubs/resource.json")
    +				.build();
    +		// We're asserting if WireMock responded properly
    +		assertThat(this.service.go()).isEqualTo("Hello World");
    +		server.verify();
    +	}
    +
    +}

    The baseUrl value is prepended to all mock calls, and the stubs() method takes a stub +path resource pattern as an argument. In the preceding example, the stub defined at +/stubs/resource.json is loaded into the mock server. If the RestTemplate is asked to +visit https://example.org/, it gets the responses as being declared at that URL. More +than one stub pattern can be specified, and each one can be a directory (for a recursive +list of all ".json"), a fixed filename (as in the example above), or an Ant-style +pattern. The JSON format is the normal WireMock format, which you can read about in the +WireMock website.

    Currently, the Spring Cloud Contract Verifier supports Tomcat, Jetty, and Undertow as +Spring Boot embedded servers, and Wiremock itself has "native" support for a particular +version of Jetty (currently 9.2). To use the native Jetty, you need to add the native +Wiremock dependencies and exclude the Spring Boot container (if there is one).

    97.6 Customization of WireMock configuration

    You can register a bean of org.springframework.cloud.contract.wiremock.WireMockConfigurationCustomizer type +in order to customize the WireMock configuration (e.g. add custom transformers). +Example:

    		@Bean
    +		WireMockConfigurationCustomizer optionsCustomizer() {
    +			return new WireMockConfigurationCustomizer() {
    +				@Override
    +				public void customize(WireMockConfiguration options) {
    +// perform your customization here
    +				}
    +			};
    +		}

    97.7 Generating Stubs using REST Docs

    Spring REST Docs can be used to generate +documentation (for example in Asciidoctor format) for an HTTP API with Spring MockMvc +or WebTestClient or Rest Assured. At the same time that you generate documentation for your API, you can also +generate WireMock stubs by using Spring Cloud Contract WireMock. To do so, write your +normal REST Docs test cases and use @AutoConfigureRestDocs to have stubs be +automatically generated in the REST Docs output directory. The following code shows an +example using MockMvc:

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureRestDocs(outputDir = "target/snippets")
    +@AutoConfigureMockMvc
    +public class ApplicationTests {
    +
    +	@Autowired
    +	private MockMvc mockMvc;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		mockMvc.perform(get("/resource"))
    +				.andExpect(content().string("Hello World"))
    +				.andDo(document("resource"));
    +	}
    +}

    This test generates a WireMock stub at "target/snippets/stubs/resource.json". It matches +all GET requests to the "/resource" path. The same example with WebTestClient (used +for testing Spring WebFlux applications) would look like this:

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureRestDocs(outputDir = "target/snippets")
    +@AutoConfigureWebTestClient
    +public class ApplicationTests {
    +
    +	@Autowired
    +	private WebTestClient client;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		client.get().uri("/resource").exchange()
    +				.expectBody(String.class).isEqualTo("Hello World")
    + 				.consumeWith(document("resource"));
    +	}
    +}

    Without any additional configuration, these tests create a stub with a request matcher +for the HTTP method and all headers except "host" and "content-length". To match the +request more precisely (for example, to match the body of a POST or PUT), we need to +explicitly create a request matcher. Doing so has two effects:

    • Creating a stub that matches only in the way you specify.
    • Asserting that the request in the test case also matches the same conditions.

    The main entry point for this feature is WireMockRestDocs.verify(), which can be used +as a substitute for the document() convenience method, as shown in the following +example:

    import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify;
    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureRestDocs(outputDir = "target/snippets")
    +@AutoConfigureMockMvc
    +public class ApplicationTests {
    +
    +	@Autowired
    +	private MockMvc mockMvc;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		mockMvc.perform(post("/resource")
    +                .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
    +				.andExpect(status().isOk())
    +				.andDo(verify().jsonPath("$.id"))
    +                .andDo(document("resource"));
    +	}
    +}

    This contract specifies that any valid POST with an "id" field receives the response +defined in this test. You can chain together calls to .jsonPath() to add additional +matchers. If JSON Path is unfamiliar, The JayWay +documentation can help you get up to speed. The WebTestClient version of this test +has a similar verify() static helper that you insert in the same place.

    Instead of the jsonPath and contentType convenience methods, you can also use the +WireMock APIs to verify that the request matches the created stub, as shown in the +following example:

    @Test
    +public void contextLoads() throws Exception {
    +	mockMvc.perform(post("/resource")
    +               .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
    +			.andExpect(status().isOk())
    +			.andDo(verify()
    +					.wiremock(WireMock.post(
    +						urlPathEquals("/resource"))
    +						.withRequestBody(matchingJsonPath("$.id")))
    +                       .andDo(document("post-resource"));
    +}

    The WireMock API is rich. You can match headers, query parameters, and request body by +regex as well as by JSON path. These features can be used to create stubs with a wider +range of parameters. The above example generates a stub resembling the following example:

    post-resource.json.  +

    {
    +  "request" : {
    +    "url" : "/resource",
    +    "method" : "POST",
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$.id"
    +    }]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "Hello World",
    +    "headers" : {
    +      "X-Application-Context" : "application:-1",
    +      "Content-Type" : "text/plain"
    +    }
    +  }
    +}

    +

    [Note]Note

    You can use either the wiremock() method or the jsonPath() and contentType() +methods to create request matchers, but you can’t use both approaches.

    On the consumer side, you can make the resource.json generated earlier in this section +available on the classpath (by +<<publishing-stubs-as-jars], for example). After that, you can create a stub using WireMock in a +number of different ways, including by using +@AutoConfigureWireMock(stubs="classpath:resource.json"), as described earlier in this +document.

    97.8 Generating Contracts by Using REST Docs

    You can also generate Spring Cloud Contract DSL files and documentation with Spring REST +Docs. If you do so in combination with Spring Cloud WireMock, you get both the contracts +and the stubs.

    Why would you want to use this feature? Some people in the community asked questions +about a situation in which they would like to move to DSL-based contract definition, +but they already have a lot of Spring MVC tests. Using this feature lets you generate +the contract files that you can later modify and move to folders (defined in your +configuration) so that the plugin finds them.

    [Tip]Tip

    You might wonder why this functionality is in the WireMock module. The functionality +is there because it makes sense to generate both the contracts and the stubs.

    Consider the following test:

    		this.mockMvc
    +				.perform(post("/foo").accept(MediaType.APPLICATION_PDF)
    +						.accept(MediaType.APPLICATION_JSON)
    +						.contentType(MediaType.APPLICATION_JSON)
    +						.content("{\"foo\": 23, \"bar\" : \"baz\" }"))
    +				.andExpect(status().isOk()).andExpect(content().string("bar"))
    +				// first WireMock
    +				.andDo(WireMockRestDocs.verify().jsonPath("$[?(@.foo >= 20)]")
    +						.jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]")
    +						.contentType(MediaType.valueOf("application/json")))
    +				// then Contract DSL documentation
    +				.andDo(document("index", SpringCloudContractRestDocs.dslContract()));

    The preceding test creates the stub presented in the previous section, generating both +the contract and a documentation file.

    The contract is called index.groovy and might look like the following example:

    import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +    request {
    +        method 'POST'
    +        url '/foo'
    +        body('''
    +            {"foo": 23 }
    +        ''')
    +        headers {
    +            header('''Accept''', '''application/json''')
    +            header('''Content-Type''', '''application/json''')
    +        }
    +    }
    +    response {
    +        status OK()
    +        body('''
    +        bar
    +        ''')
    +        headers {
    +            header('''Content-Type''', '''application/json;charset=UTF-8''')
    +            header('''Content-Length''', '''3''')
    +        }
    +        testMatchers {
    +            jsonPath('$[?(@.foo >= 20)]', byType())
    +        }
    +    }
    +}

    The generated document (formatted in Asciidoc in this case) contains a formatted +contract. The location of this file would be index/dsl-contract.adoc.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_for_cloud_foundry.html b/Greenwich.SR5/multi/multi__spring_cloud_for_cloud_foundry.html new file mode 100644 index 00000000..ac049d3d --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_for_cloud_foundry.html @@ -0,0 +1,18 @@ + + + Part XII. Spring Cloud for Cloud Foundry

    Part XII. Spring Cloud for Cloud Foundry

    Spring Cloud for Cloudfoundry makes it easy to run +Spring Cloud apps in +Cloud Foundry (the Platform as a +Service). Cloud Foundry has the notion of a "service", which is +middlware that you "bind" to an app, essentially providing it with an +environment variable containing credentials (e.g. the location and +username to use for the service).

    The spring-cloud-cloudfoundry-commons module configures the +Reactor-based Cloud Foundry Java client, v 3.0, and can be used standalone.

    The spring-cloud-cloudfoundry-web project provides basic support for +some enhanced features of webapps in Cloud Foundry: binding +automatically to single-sign-on services and optionally enabling +sticky routing for discovery.

    The spring-cloud-cloudfoundry-discovery project provides an +implementation of Spring Cloud Commons DiscoveryClient so you can +@EnableDiscoveryClient and provide your credentials as +spring.cloud.cloudfoundry.discovery.[username,password] (also *.url if you are not connecting to Pivotal Web Services) and then you +can use the DiscoveryClient directly or via a LoadBalancerClient.

    The first time you use it the discovery client might be slow owing to +the fact that it has to get an access token from Cloud Foundry.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_function_2.html b/Greenwich.SR5/multi/multi__spring_cloud_function_2.html new file mode 100644 index 00000000..2b34b7a5 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_function_2.html @@ -0,0 +1,3 @@ + + + Part XVI. Spring Cloud Function

    Part XVI. Spring Cloud Function

    Mark Fisher, Dave Syer, Oleg Zhurakousky

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_gateway.html b/Greenwich.SR5/multi/multi__spring_cloud_gateway.html new file mode 100644 index 00000000..637f6ea6 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_gateway.html @@ -0,0 +1,3 @@ + + + Part XV. Spring Cloud Gateway

    Part XV. Spring Cloud Gateway

    Greenwich.SR5

    This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_kubernetes.html b/Greenwich.SR5/multi/multi__spring_cloud_kubernetes.html new file mode 100644 index 00000000..5d5cb44d --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_kubernetes.html @@ -0,0 +1,3 @@ + + + Part XVII. Spring Cloud Kubernetes

    Part XVII. Spring Cloud Kubernetes

    This reference guide covers how to use Spring Cloud Kubernetes.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_netflix.html b/Greenwich.SR5/multi/multi__spring_cloud_netflix.html new file mode 100644 index 00000000..93d84cd7 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_netflix.html @@ -0,0 +1,8 @@ + + + Part III. Spring Cloud Netflix

    Part III. Spring Cloud Netflix

    Greenwich.SR5

    This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms. With a few +simple annotations you can quickly enable and configure the common patterns inside your +application and build large distributed systems with battle-tested Netflix components. The +patterns provided include Service Discovery (Eureka), Circuit Breaker (Hystrix), +Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_openfeign.html b/Greenwich.SR5/multi/multi__spring_cloud_openfeign.html new file mode 100644 index 00000000..8e3a7313 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_openfeign.html @@ -0,0 +1,4 @@ + + + Part IV. Spring Cloud OpenFeign

    Part IV. Spring Cloud OpenFeign

    Greenwich.SR5

    This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_security.html b/Greenwich.SR5/multi/multi__spring_cloud_security.html new file mode 100644 index 00000000..8de29ae1 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_security.html @@ -0,0 +1,11 @@ + + + Part XI. Spring Cloud Security

    Part XI. Spring Cloud Security

    Spring Cloud Security offers a set of primitives for building secure +applications and services with minimum fuss. A declarative model which +can be heavily configured externally (or centrally) lends itself to +the implementation of large systems of co-operating, remote components, +usually with a central indentity management service. It is also extremely +easy to use in a service platform like Cloud Foundry. Building on +Spring Boot and Spring Security OAuth2 we can quickly create systems that +implement common patterns like single sign on, token relay and token +exchange.

    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_sleuth.html b/Greenwich.SR5/multi/multi__spring_cloud_sleuth.html new file mode 100644 index 00000000..7ad3046a --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_sleuth.html @@ -0,0 +1,3 @@ + + + Part VIII. Spring Cloud Sleuth

    Part VIII. Spring Cloud Sleuth

    Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant

    Greenwich.SR5

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_sleuth_2.html b/Greenwich.SR5/multi/multi__spring_cloud_sleuth_2.html new file mode 100644 index 00000000..747c4778 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_sleuth_2.html @@ -0,0 +1,27 @@ + + + 160. Spring Cloud Sleuth

    160. Spring Cloud Sleuth

    Spring Cloud Sleuth is an instrumentation framework for Spring Boot applications. +It captures trace information and can forward traces to services like Zipkin for storage and analysis.

    Google Cloud Platform provides its own managed distributed tracing service called Stackdriver Trace. +Instead of running and maintaining your own Zipkin instance and storage, you can use Stackdriver Trace to store traces, view trace details, generate latency distributions graphs, and generate performance regression reports.

    This Spring Cloud GCP starter can forward Spring Cloud Sleuth traces to Stackdriver Trace without an intermediary Zipkin server.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-trace</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-trace'
    +}

    You must enable Stackdriver Trace API from the Google Cloud Console in order to capture traces. +Navigate to the Stackdriver Trace API for your project and make sure it’s enabled.

    [Note]Note

    If you are already using a Zipkin server capturing trace information from multiple platform/frameworks, you can also use a Stackdriver Zipkin proxy to forward those traces to Stackdriver Trace without modifying existing applications.

    160.1 Tracing

    Spring Cloud Sleuth uses the Brave tracer to generate traces. +This integration enables Brave to use the StackdriverTracePropagation propagation.

    A propagation is responsible for extracting trace context from an entity (e.g., an HTTP servlet request) and injecting trace context into an entity. +A canonical example of the propagation usage is a web server that receives an HTTP request, which triggers other HTTP requests from the server before returning an HTTP response to the original caller. +In the case of StackdriverTracePropagation, first it looks for trace context in the x-cloud-trace-context key (e.g., an HTTP request header). +The value of the x-cloud-trace-context key can be formatted in three different ways:

    • x-cloud-trace-context: TRACE_ID
    • x-cloud-trace-context: TRACE_ID/SPAN_ID
    • x-cloud-trace-context: TRACE_ID/SPAN_ID;o=TRACE_TRUE

    TRACE_ID is a 32-character hexadecimal value that encodes a 128-bit number.

    SPAN_ID is an unsigned long. +Since Stackdriver Trace doesn’t support span joins, a new span ID is always generated, regardless of the one specified in x-cloud-trace-context.

    TRACE_TRUE can either be 0 if the entity should be untraced, or 1 if it should be traced. +This field forces the decision of whether or not to trace the request; if omitted then the decision is deferred to the sampler.

    If a x-cloud-trace-context key isn’t found, StackdriverTracePropagation falls back to tracing with the X-B3 headers.

    160.2 Spring Boot Starter for Stackdriver Trace

    Spring Boot Starter for Stackdriver Trace uses Spring Cloud Sleuth and auto-configures a StackdriverSender that sends the Sleuth’s trace information to Stackdriver Trace.

    All configurations are optional:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.trace.enabled

    Auto-configure Spring Cloud Sleuth to send traces to Stackdriver Trace.

    No

    true

    spring.cloud.gcp.trace.project-id

    Overrides the project ID from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.credentials.location

    Overrides the credentials location from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.credentials.encoded-key

    Overrides the credentials encoded key from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.credentials.scopes

    Overrides the credentials scopes from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.num-executor-threads

    Number of threads used by the Trace executor

    No

    4

    spring.cloud.gcp.trace.authority

    HTTP/2 authority the channel claims to be connecting to.

    No

     

    spring.cloud.gcp.trace.compression

    Name of the compression to use in Trace calls

    No

     

    spring.cloud.gcp.trace.deadline-ms

    Call deadline in milliseconds

    No

     

    spring.cloud.gcp.trace.max-inbound-size

    Maximum size for inbound messages

    No

     

    spring.cloud.gcp.trace.max-outbound-size

    Maximum size for outbound messages

    No

     

    spring.cloud.gcp.trace.wait-for-ready

    Waits for the channel to be ready in case of a transient failure

    No

    false

    spring.cloud.gcp.trace.messageTimeout

    Timeout in seconds before pending spans will be sent in batches to GCP Stackdriver Trace. Added for forward compatibility.

    No

    spring.zipkin.messageTimeout

    You can use core Spring Cloud Sleuth properties to control Sleuth’s sampling rate, etc. +Read Sleuth documentation for more information on Sleuth configurations.

    For example, when you are testing to see the traces are going through, you can set the sampling rate to 100%.

    spring.sleuth.sampler.probability=1                     # Send 100% of the request traces to Stackdriver.
    +spring.sleuth.web.skipPattern=(^cleanup.*|.+favicon.*)  # Ignore some URL paths.

    Spring Cloud GCP Trace does override some Sleuth configurations:

    • Always uses 128-bit Trace IDs. +This is required by Stackdriver Trace.
    • Does not use Span joins. +Span joins will share the span ID between the client and server Spans. +Stackdriver requires that every Span ID within a Trace to be unique, so Span joins are not supported.
    • Uses StackdriverHttpClientParser and StackdriverHttpServerParser by default to populate Stackdriver related fields.

    160.3 Overriding the auto-configuration

    Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. +In order to get this to work, every tracing system needs to have a Reporter<Span> and Sender. +If you want to override the provided beans you need to give them a specific name. +To do this you can use respectively StackdriverTraceAutoConfiguration.REPORTER_BEAN_NAME and StackdriverTraceAutoConfiguration.SENDER_BEAN_NAME.

    160.4 Integration with Logging

    Integration with Stackdriver Logging is available through the Stackdriver Logging Support. +If the Trace integration is used together with the Logging one, the request logs will be associated to the corresponding traces. +The trace logs can be viewed by going to the Google Cloud Console Trace List, selecting a trace and pressing the Logs → View link in the Details section.

    160.5 Sample

    A sample application and a codelab are available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_stream.html b/Greenwich.SR5/multi/multi__spring_cloud_stream.html new file mode 100644 index 00000000..3d4c3948 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_stream.html @@ -0,0 +1,3 @@ + + + Part V. Spring Cloud Stream

    Part V. Spring Cloud Stream

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_stream_2.html b/Greenwich.SR5/multi/multi__spring_cloud_stream_2.html new file mode 100644 index 00000000..7b1e3fbb --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_stream_2.html @@ -0,0 +1,21 @@ + + + 159. Spring Cloud Stream

    159. Spring Cloud Stream

    Spring Cloud GCP provides a Spring Cloud Stream binder to Google Cloud Pub/Sub.

    The provided binder relies on the Spring Integration Channel Adapters for Google Cloud Pub/Sub.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-pubsub-stream-binder</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub-stream-binder'
    +}

    159.1 Overview

    This binder binds producers to Google Cloud Pub/Sub topics and consumers to subscriptions.

    [Note]Note

    Partitioning is currently not supported by this binder.

    159.2 Configuration

    You can configure the Spring Cloud Stream Binder for Google Cloud Pub/Sub to automatically generate the underlying resources, like the Google Cloud Pub/Sub topics and subscriptions for producers and consumers. +For that, you can use the spring.cloud.stream.gcp.pubsub.bindings.<channelName>.<consumer|producer>.auto-create-resources property, which is turned ON by default.

    Starting with version 1.1, these and other binder properties can be configured globally for all the bindings, e.g. spring.cloud.stream.gcp.pubsub.default.consumer.auto-create-resources.

    If you are using Pub/Sub auto-configuration from the Spring Cloud GCP Pub/Sub Starter, you should refer to the configuration section for other Pub/Sub parameters.

    [Note]Note

    To use this binder with a running emulator, configure its host and port via spring.cloud.gcp.pubsub.emulator-host.

    159.2.1 Producer Destination Configuration

    If automatic resource creation is turned ON and the topic corresponding to the destination name does not exist, it will be created.

    For example, for the following configuration, a topic called myEvents would be created.

    application.properties.  +

    spring.cloud.stream.bindings.events.destination=myEvents
    +spring.cloud.stream.gcp.pubsub.bindings.events.producer.auto-create-resources=true

    +

    159.2.2 Consumer Destination Configuration

    If automatic resource creation is turned ON and the subscription and/or the topic do not exist for a consumer, a subscription and potentially a topic will be created. +The topic name will be the same as the destination name, and the subscription name will be the destination name followed by the consumer group name.

    Regardless of the auto-create-resources setting, if the consumer group is not specified, an anonymous one will be created with the name anonymous.<destinationName>.<randomUUID>. +Then when the binder shuts down, all Pub/Sub subscriptions created for anonymous consumer groups will be automatically cleaned up.

    For example, for the following configuration, a topic named myEvents and a subscription called myEvents.counsumerGroup1 would be created. +If the consumer group is not specified, a subscription called anonymous.myEvents.a6d83782-c5a3-4861-ac38-e6e2af15a7be would be created and later cleaned up.

    [Important]Important

    If you are manually creating Pub/Sub subscriptions for consumers, make sure that they follow the naming convention of <destinationName>.<consumerGroup>.

    application.properties.  +

    spring.cloud.stream.bindings.events.destination=myEvents
    +spring.cloud.stream.gcp.pubsub.bindings.events.consumer.auto-create-resources=true
    +
    +# specify consumer group, and avoid anonymous consumer group generation
    +spring.cloud.stream.bindings.events.group=consumerGroup1

    +

    159.3 Sample

    A sample application is available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_vault.html b/Greenwich.SR5/multi/multi__spring_cloud_vault.html new file mode 100644 index 00000000..099d6678 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_vault.html @@ -0,0 +1,3 @@ + + + Part XIV. Spring Cloud Vault

    Part XIV. Spring Cloud Vault

    © 2016-2019 The original authors.

    [Note]Note

    Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

    Spring Cloud Vault Config provides client-side support for externalized configuration in a distributed system. With HashiCorp’s Vault you have a central place to manage external secret properties for applications across all environments. Vault can manage static and dynamic secrets such as username/password for remote applications/resources and provide credentials for external services such as MySQL, PostgreSQL, Apache Cassandra, MongoDB, Consul, AWS and more.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_cloud_zookeeper.html b/Greenwich.SR5/multi/multi__spring_cloud_zookeeper.html new file mode 100644 index 00000000..2e0418c4 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_cloud_zookeeper.html @@ -0,0 +1,9 @@ + + + Part X. Spring Cloud Zookeeper

    Part X. Spring Cloud Zookeeper

    This project provides Zookeeper integrations for Spring Boot applications through +autoconfiguration and binding to the Spring Environment and other Spring programming model +idioms. With a few annotations, you can quickly enable and configure the common patterns +inside your application and build large distributed systems with Zookeeper based +components. The provided patterns include Service Discovery and Configuration. Integration +with Spring Cloud Netflix provides Intelligent Routing (Zuul), Client Side Load Balancing +(Ribbon), and Circuit Breaker (Hystrix).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_data_cloud_datastore.html b/Greenwich.SR5/multi/multi__spring_data_cloud_datastore.html new file mode 100644 index 00000000..73aadff1 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_data_cloud_datastore.html @@ -0,0 +1,467 @@ + + + 164. Spring Data Cloud Datastore

    164. Spring Data Cloud Datastore

    Spring Data is an abstraction for storing and retrieving POJOs in numerous storage technologies. +Spring Cloud GCP adds Spring Data support for Google Cloud Datastore.

    Maven coordinates for this module only, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-data-datastore</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-datastore'
    +}

    We provide a Spring Boot Starter for Spring Data Datastore, with which you can use our recommended auto-configuration setup. +To use the starter, see the coordinates below.

    Maven:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-data-datastore</artifactId>
    +</dependency>

    Gradle:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-datastore'
    +}

    This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Datastore libraries as well.

    164.1 Configuration

    To setup Spring Data Cloud Datastore, you have to configure the following:

    • Setup the connection details to Google Cloud Datastore.

    164.1.1 Cloud Datastore settings

    You can the use Spring Boot Starter for Spring Data Datastore to autoconfigure Google Cloud Datastore in your Spring application. +It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project. +The following configuration options are available:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.datastore.enabled

    Enables the Cloud Datastore client

    No

    true

    spring.cloud.gcp.datastore.project-id

    GCP project ID where the Google Cloud Datastore API is hosted, if different from the one in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.datastore.credentials.location

    OAuth2 credentials for authenticating with the +Google Cloud Datastore API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.datastore.credentials.encoded-key

    Base64-encoded OAuth2 credentials for authenticating with the +Google Cloud Datastore API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.datastore.credentials.scopes

    OAuth2 scope for Spring Cloud GCP +Cloud Datastore credentials

    No

    https://www.googleapis.com/auth/datastore

    spring.cloud.gcp.datastore.namespace

    The Cloud Datastore namespace to use

    No

    the Default namespace of Cloud Datastore in your GCP project

    164.1.2 Repository settings

    Spring Data Repositories can be configured via the @EnableDatastoreRepositories annotation on your main @Configuration class. +With our Spring Boot Starter for Spring Data Cloud Datastore, @EnableDatastoreRepositories is automatically added. +It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by @EnableDatastoreRepositories.

    164.1.3 Autoconfiguration

    Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:

    • an instance of DatastoreTemplate
    • an instance of all user defined repositories extending CrudRepository, PagingAndSortingRepository, and DatastoreRepository (an extension of PagingAndSortingRepository with additional Cloud Datastore features) when repositories are enabled
    • an instance of Datastore from the Google Cloud Java Client for Datastore, for convenience and lower level API access

    164.2 Object Mapping

    Spring Data Cloud Datastore allows you to map domain POJOs to Cloud Datastore kinds and entities via annotations:

    @Entity(name = "traders")
    +public class Trader {
    +
    +	@Id
    +	@Field(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@Transient
    +	Double temporaryNumber;
    +}

    Spring Data Cloud Datastore will ignore any property annotated with @Transient. +These properties will not be written to or read from Cloud Datastore.

    164.2.1 Constructors

    Simple constructors are supported on POJOs. +The constructor arguments can be a subset of the persistent properties. +Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument. +Arguments that are not directly set to properties are not supported.

    @Entity(name = "traders")
    +public class Trader {
    +
    +	@Id
    +	@Field(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@Transient
    +	Double temporaryNumber;
    +
    +	public Trader(String traderId, String firstName) {
    +	    this.traderId = traderId;
    +	    this.firstName = firstName;
    +	}
    +}

    164.2.2 Kind

    The @Entity annotation can provide the name of the Cloud Datastore kind that stores instances of the annotated class, one per row.

    164.2.3 Keys

    @Id identifies the property corresponding to the ID value.

    You must annotate one of your POJO’s fields as the ID value, because every entity in Cloud Datastore requires a single ID value:

    @Entity(name = "trades")
    +public class Trade {
    +	@Id
    +	@Field(name = "trade_id")
    +	String tradeId;
    +
    +	@Field(name = "trader_id")
    +	String traderId;
    +
    +	String action;
    +
    +	Double price;
    +
    +	Double shares;
    +
    +	String symbol;
    +}

    Datastore can automatically allocate integer ID values. +If a POJO instance with a Long ID property is written to Cloud Datastore with null as the ID value, then Spring Data Cloud Datastore will obtain a newly allocated ID value from Cloud Datastore and set that in the POJO for saving. +Because primitive long ID properties cannot be null and default to 0, keys will not be allocated.

    164.2.4 Fields

    All accessible properties on POJOs are automatically recognized as a Cloud Datastore field. +Field naming is generated by the PropertyNameFieldNamingStrategy by default defined on the DatastoreMappingContext bean. +The @Field annotation optionally provides a different field name than that of the property.

    164.2.5 Supported Types

    Spring Data Cloud Datastore supports the following types for regular fields and elements of collections:

    TypeStored as

    com.google.cloud.Timestamp

    com.google.cloud.datastore.TimestampValue

    com.google.cloud.datastore.Blob

    com.google.cloud.datastore.BlobValue

    com.google.cloud.datastore.LatLng

    com.google.cloud.datastore.LatLngValue

    java.lang.Boolean, boolean

    com.google.cloud.datastore.BooleanValue

    java.lang.Double, double

    com.google.cloud.datastore.DoubleValue

    java.lang.Long, long

    com.google.cloud.datastore.LongValue

    java.lang.Integer, int

    com.google.cloud.datastore.LongValue

    java.lang.String

    com.google.cloud.datastore.StringValue

    com.google.cloud.datastore.Entity

    com.google.cloud.datastore.EntityValue

    com.google.cloud.datastore.Key

    com.google.cloud.datastore.KeyValue

    byte[]

    com.google.cloud.datastore.BlobValue

    Java enum values

    com.google.cloud.datastore.StringValue

    In addition, all types that can be converted to the ones listed in the table by +org.springframework.core.convert.support.DefaultConversionService are supported.

    164.2.6 Custom types

    Custom converters can be used extending the type support for user defined types.

    1. Converters need to implement the org.springframework.core.convert.converter.Converter interface in both directions.
    2. The user defined type needs to be mapped to one of the basic types supported by Cloud Datastore.
    3. An instance of both Converters (read and write) needs to be passed to the DatastoreCustomConversions constructor, which then has to be made available as a @Bean for DatastoreCustomConversions.

    For example:

    We would like to have a field of type Album on our Singer POJO and want it to be stored as a string property:

    @Entity
    +public class Singer {
    +
    +	@Id
    +	String singerId;
    +
    +	String name;
    +
    +	Album album;
    +}

    Where Album is a simple class:

    public class Album {
    +	String albumName;
    +
    +	LocalDate date;
    +}

    We have to define the two converters:

    	//Converter to write custom Album type
    +	static final Converter<Album, String> ALBUM_STRING_CONVERTER =
    +			new Converter<Album, String>() {
    +				@Override
    +				public String convert(Album album) {
    +					return album.getAlbumName() + " " + album.getDate().format(DateTimeFormatter.ISO_DATE);
    +				}
    +			};
    +
    +	//Converters to read custom Album type
    +	static final Converter<String, Album> STRING_ALBUM_CONVERTER =
    +			new Converter<String, Album>() {
    +				@Override
    +				public Album convert(String s) {
    +					String[] parts = s.split(" ");
    +					return new Album(parts[0], LocalDate.parse(parts[parts.length - 1], DateTimeFormatter.ISO_DATE));
    +				}
    +			};

    That will be configured in our @Configuration file:

    @Configuration
    +public class ConverterConfiguration {
    +	@Bean
    +	public DatastoreCustomConversions datastoreCustomConversions() {
    +		return new DatastoreCustomConversions(
    +				Arrays.asList(
    +						ALBUM_STRING_CONVERTER,
    +						STRING_ALBUM_CONVERTER));
    +	}
    +}

    164.2.7 Collections and arrays

    Arrays and collections (types that implement java.util.Collection) of supported types are supported. +They are stored as com.google.cloud.datastore.ListValue. +Elements are converted to Cloud Datastore supported types individually. byte[] is an exception, it is converted to +com.google.cloud.datastore.Blob.

    164.2.8 Custom Converter for collections

    Users can provide converters from List<?> to the custom collection type. +Only read converter is necessary, the Collection API is used on the write side to convert a collection to the internal list type.

    Collection converters need to implement the org.springframework.core.convert.converter.Converter interface.

    Example:

    Let’s improve the Singer class from the previous example. +Instead of a field of type Album, we would like to have a field of type ImmutableSet<Album>:

    @Entity
    +public class Singer {
    +
    +	@Id
    +	String singerId;
    +
    +	String name;
    +
    +	ImmutableSet<Album> albums;
    +}

    We have to define a read converter only:

    static final Converter<List<?>, ImmutableSet<?>> LIST_IMMUTABLE_SET_CONVERTER =
    +			new Converter<List<?>, ImmutableSet<?>>() {
    +				@Override
    +				public ImmutableSet<?> convert(List<?> source) {
    +					return ImmutableSet.copyOf(source);
    +				}
    +			};

    And add it to the list of custom converters:

    @Configuration
    +public class ConverterConfiguration {
    +	@Bean
    +	public DatastoreCustomConversions datastoreCustomConversions() {
    +		return new DatastoreCustomConversions(
    +				Arrays.asList(
    +						LIST_IMMUTABLE_SET_CONVERTER,
    +
    +						ALBUM_STRING_CONVERTER,
    +						STRING_ALBUM_CONVERTER));
    +	}
    +}

    164.3 Relationships

    There are three ways to represent relationships between entities that are described in this section:

    • Embedded entities stored directly in the field of the containing entity
    • @Descendant annotated properties for one-to-many relationships
    • @Reference annotated properties for general relationships without hierarchy

    164.3.1 Embedded Entities

    Fields whose types are also annotated with @Entity are converted to EntityValue and stored inside the parent entity.

    Here is an example of Cloud Datastore entity containing an embedded entity in JSON:

    {
    +  "name" : "Alexander",
    +  "age" : 47,
    +  "child" : {"name" : "Philip"  }
    +}

    This corresponds to a simple pair of Java entities:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity("parents")
    +public class Parent {
    +  @Id
    +  String name;
    +
    +  Child child;
    +}
    +
    +@Entity
    +public class Child {
    +  String name;
    +}

    Child entities are not stored in their own kind. +They are stored in their entirety in the child field of the parents kind.

    Multiple levels of embedded entities are supported.

    [Note]Note

    Embedded entities don’t need to have @Id field, it is only required for top level entities.

    Example:

    Entities can hold embedded entities that are their own type. +We can store trees in Cloud Datastore using this feature:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded;
    +import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity
    +public class EmbeddableTreeNode {
    +  @Id
    +  long value;
    +
    +  EmbeddableTreeNode left;
    +
    +  EmbeddableTreeNode right;
    +
    +  Map<String, Long> longValues;
    +
    +  Map<String, List<Timestamp>> listTimestamps;
    +
    +  public EmbeddableTreeNode(long value, EmbeddableTreeNode left, EmbeddableTreeNode right) {
    +    this.value = value;
    +    this.left = left;
    +    this.right = right;
    +  }
    +}

    Maps

    Maps will be stored as embedded entities where the key values become the field names in the embedded entity. +The value types in these maps can be any regularly supported property type, and the key values will be converted to String using the configured converters.

    Also, a collection of entities can be embedded; it will be converted to ListValue on write.

    Example:

    Instead of a binary tree from the previous example, we would like to store a general tree +(each node can have an arbitrary number of children) in Cloud Datastore. +To do that, we need to create a field of type List<EmbeddableTreeNode>:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded;
    +import org.springframework.data.annotation.Id;
    +
    +public class EmbeddableTreeNode {
    +  @Id
    +  long value;
    +
    +  List<EmbeddableTreeNode> children;
    +
    +  Map<String, EmbeddableTreeNode> siblingNodes;
    +
    +  Map<String, Set<EmbeddableTreeNode>> subNodeGroups;
    +
    +  public EmbeddableTreeNode(List<EmbeddableTreeNode> children) {
    +    this.children = children;
    +  }
    +}

    Because Maps are stored as entities, they can further hold embedded entities:

    • Singular embedded objects in the value can be stored in the values of embedded Maps.
    • Collections of embedded objects in the value can also be stored as the values of embedded Maps.
    • Maps in the value are further stored as embedded entities with the same rules applied recursively for their values.

    164.3.2 Ancestor-Descendant Relationships

    Parent-child relationships are supported via the @Descendants annotation.

    Unlike embedded children, descendants are fully-formed entities residing in their own kinds. +The parent entity does not have an extra field to hold the descendant entities. +Instead, the relationship is captured in the descendants' keys, which refer to their parent entities:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Descendants;
    +import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity("orders")
    +public class ShoppingOrder {
    +  @Id
    +  long id;
    +
    +  @Descendants
    +  List<Item> items;
    +}
    +
    +@Entity("purchased_item")
    +public class Item {
    +  @Id
    +  Key purchasedItemKey;
    +
    +  String name;
    +
    +  Timestamp timeAddedToOrder;
    +}

    For example, an instance of a GQL key-literal representation for Item would also contain the parent ShoppingOrder ID value:

    Key(orders, '12345', purchased_item, 'eggs')

    The GQL key-literal representation for the parent ShoppingOrder would be:

    Key(orders, '12345')

    The Cloud Datastore entities exist separately in their own kinds.

    The ShoppingOrder:

    {
    +  "id" : 12345
    +}

    The two items inside that order:

    {
    +  "purchasedItemKey" : Key(orders, '12345', purchased_item, 'eggs'),
    +  "name" : "eggs",
    +  "timeAddedToOrder" : "2014-09-27 12:30:00.45-8:00"
    +}
    +
    +{
    +  "purchasedItemKey" : Key(orders, '12345', purchased_item, 'sausage'),
    +  "name" : "sausage",
    +  "timeAddedToOrder" : "2014-09-28 11:30:00.45-9:00"
    +}

    The parent-child relationship structure of objects is stored in Cloud Datastore using Datastore’s ancestor relationships. +Because the relationships are defined by the Ancestor mechanism, there is no extra column needed in either the parent or child entity to store this relationship. +The relationship link is part of the descendant entity’s key value. +These relationships can be many levels deep.

    Properties holding child entities must be collection-like, but they can be any of the supported inter-convertible collection-like types that are supported for regular properties such as List, arrays, Set, etc…​ +Child items must have Key as their ID type because Cloud Datastore stores the ancestor relationship link inside the keys of the children.

    Reading or saving an entity automatically causes all subsequent levels of children under that entity to be read or saved, respectively. +If a new child is created and added to a property annotated @Descendants and the key property is left null, then a new key will be allocated for that child. +The ordering of the retrieved children may not be the same as the ordering in the original property that was saved.

    Child entities cannot be moved from the property of one parent to that of another unless the child’s key property is set to null or a value that contains the new parent as an ancestor. +Since Cloud Datastore entity keys can have multiple parents, it is possible that a child entity appears in the property of multiple parent entities. +Because entity keys are immutable in Cloud Datastore, to change the key of a child you must delete the existing one and re-save it with the new key.

    164.3.3 Key Reference Relationships

    General relationships can be stored using the @Reference annotation.

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Reference;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity
    +public class ShoppingOrder {
    +  @Id
    +  long id;
    +
    +  @Reference
    +  List<Item> items;
    +
    +  @Reference
    +  Item specialSingleItem;
    +}
    +
    +@Entity
    +public class Item {
    +  @Id
    +  Key purchasedItemKey;
    +
    +  String name;
    +
    +  Timestamp timeAddedToOrder;
    +}

    @Reference relationships are between fully-formed entities residing in their own kinds. +The relationship between ShoppingOrder and Item entities are stored as a Key field inside ShoppingOrder, which are resolved to the underlying Java entity type by Spring Data Cloud Datastore:

    {
    +  "id" : 12345,
    +  "specialSingleItem" : Key(item, "milk"),
    +  "items" : [ Key(item, "eggs"), Key(item, "sausage") ]
    +}

    Reference properties can either be singular or collection-like. +These properties correspond to actual columns in the entity and Cloud Datastore Kind that hold the key values of the referenced entities. +The referenced entities are full-fledged entities of other Kinds.

    Similar to the @Descendants relationships, reading or writing an entity will recursively read or write all of the referenced entities at all levels. +If referenced entities have null ID values, then they will be saved as new entities and will have ID values allocated by Cloud Datastore. +There are no requirements for relationships between the key of an entity and the keys that entity holds as references. +The order of collection-like reference properties is not preserved when reading back from Cloud Datastore.

    164.4 Datastore Operations & Template

    DatastoreOperations and its implementation, DatastoreTemplate, provides the Template pattern familiar to Spring developers.

    Using the auto-configuration provided by Spring Boot Starter for Datastore, your Spring application context will contain a fully configured DatastoreTemplate object that you can autowire in your application:

    @SpringBootApplication
    +public class DatastoreTemplateExample {
    +
    +	@Autowired
    +	DatastoreTemplate datastoreTemplate;
    +
    +	public void doSomething() {
    +		this.datastoreTemplate.deleteAll(Trader.class);
    +		//...
    +		Trader t = new Trader();
    +		//...
    +		this.datastoreTemplate.save(t);
    +		//...
    +		List<Trader> traders = datastoreTemplate.findAll(Trader.class);
    +		//...
    +	}
    +}

    The Template API provides convenience methods for:

    • Write operations (saving and deleting)
    • Read-write transactions

    164.4.1 GQL Query

    In addition to retrieving entities by their IDs, you can also submit queries.

      <T> Iterable<T> query(Query<? extends BaseEntity> query, Class<T> entityClass);
    +
    +  <A, T> Iterable<T> query(Query<A> query, Function<A, T> entityFunc);
    +
    +  Iterable<Key> queryKeys(Query<Key> query);

    These methods, respectively, allow querying for: +* entities mapped by a given entity class using all the same mapping and converting features +* arbitrary types produced by a given mapping function +* only the Cloud Datastore keys of the entities found by the query

    164.4.2 Find by ID(s)

    Datstore reading a single entity or multiple entities in a kind.

    Using DatastoreTemplate you can execute reads, for example:

    Trader trader = this.datastoreTemplate.findById("trader1", Trader.class);
    +
    +List<Trader> traders = this.datastoreTemplate.findAllById(ImmutableList.of("trader1", "trader2"), Trader.class);
    +
    +List<Trader> allTraders = this.datastoreTemplate.findAll(Trader.class);

    Cloud Datastore executes key-based reads with strong consistency, but queries with eventual consistency. +In the example above the first two reads utilize keys, while the third is executed using a query based on the corresponding Kind of Trader.

    Indexes

    By default, all fields are indexed. +To disable indexing on a particular field, @Unindexed annotation can be used.

    Example:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Unindexed;
    +
    +public class ExampleItem {
    +	long indexedField;
    +
    +	@Unindexed
    +	long unindexedField;
    +}

    When using queries directly or via Query Methods, Cloud Datastore requires composite custom indexes if the select statement is not SELECT * or if there is more than one filtering condition in the WHERE clause.

    Read with offsets, limits, and sorting

    DatastoreRepository and custom-defined entity repositories implement the Spring Data PagingAndSortingRepository, which supports offsets and limits using page numbers and page sizes. +Paging and sorting options are also supported in DatastoreTemplate by supplying a DatastoreQueryOptions to findAll.

    Partial read

    This feature is not supported yet.

    164.4.3 Write / Update

    The write methods of DatastoreOperations accept a POJO and writes all of its properties to Datastore. +The required Datastore kind and entity metadata is obtained from the given object’s actual type.

    If a POJO was retrieved from Datastore and its ID value was changed and then written or updated, the operation will occur as if against a row with the new ID value. +The entity with the original ID value will not be affected.

    Trader t = new Trader();
    +this.datastoreTemplate.save(t);

    The save method behaves as update-or-insert.

    Partial Update

    This feature is not supported yet.

    164.4.4 Transactions

    Read and write transactions are provided by DatastoreOperations via the performTransaction method:

    @Autowired
    +DatastoreOperations myDatastoreOperations;
    +
    +public String doWorkInsideTransaction() {
    +  return myDatastoreOperations.performTransaction(
    +    transactionDatastoreOperations -> {
    +      // Work with transactionDatastoreOperations here.
    +      // It is also a DatastoreOperations object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    The performTransaction method accepts a Function that is provided an instance of a DatastoreOperations object. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular DatastoreOperations with an exception:

    • It cannot perform sub-transactions.

    Because of Cloud Datastore’s consistency guarantees, there are limitations to the operations and relationships among entities used inside transactions.

    Declarative Transactions with @Transactional Annotation

    This feature requires a bean of DatastoreTransactionManager, which is provided when using spring-cloud-gcp-starter-data-datastore.

    DatastoreTemplate and DatastoreRepository support running methods with the @Transactional annotation as transactions. +If a method annotated with @Transactional calls another method also annotated, then both methods will work within the same transaction. +performTransaction cannot be used in @Transactional annotated methods because Cloud Datastore does not support transactions within transactions.

    164.4.5 Read-Write Support for Maps

    You can work with Maps of type Map<String, ?> instead of with entity objects by directly reading and writing them to and from Cloud Datastore.

    [Note]Note

    This is a different situation than using entity objects that contain Map properties.

    The map keys are used as field names for a Datastore entity and map values are converted to Datastore supported types. +Only simple types are supported (i.e. collections are not supported). +Converters for custom value types can be added (see Section 163.2.10, “Custom types” section).

    Example:

    Map<String, Long> map = new HashMap<>();
    +map.put("field1", 1L);
    +map.put("field2", 2L);
    +map.put("field3", 3L);
    +
    +keyForMap = datastoreTemplate.createKey("kindName", "id");
    +
    +//write a map
    +datastoreTemplate.writeMap(keyForMap, map);
    +
    +//read a map
    +Map<String, Long> loadedMap = datastoreTemplate.findByIdAsMap(keyForMap, Long.class);

    164.5 Repositories

    Spring Data Repositories are an abstraction that can reduce boilerplate code.

    For example:

    public interface TraderRepository extends DatastoreRepository<Trader, String> {
    +}

    Spring Data generates a working implementation of the specified interface, which can be autowired into an application.

    The Trader type parameter to DatastoreRepository refers to the underlying domain type. +The second type parameter, String in this case, refers to the type of the key of the domain type.

    public class MyApplication {
    +
    +	@Autowired
    +	TraderRepository traderRepository;
    +
    +	public void demo() {
    +
    +		this.traderRepository.deleteAll();
    +		String traderId = "demo_trader";
    +		Trader t = new Trader();
    +		t.traderId = traderId;
    +		this.tradeRepository.save(t);
    +
    +		Iterable<Trader> allTraders = this.traderRepository.findAll();
    +
    +		int count = this.traderRepository.count();
    +	}
    +}

    Repositories allow you to define custom Query Methods (detailed in the following sections) for retrieving, counting, and deleting based on filtering and paging parameters. +Filtering parameters can be of types supported by your configured custom converters.

    164.5.1 Query methods by convention

    public interface TradeRepository extends DatastoreRepository<Trade, String[]> {
    +  List<Trader> findByAction(String action);
    +
    +  int countByAction(String action);
    +
    +  boolean existsByAction(String action);
    +
    +  List<Trade> findTop3ByActionAndSymbolAndPriceGreaterThanAndPriceLessThanOrEqualOrderBySymbolDesc(
    +  			String action, String symbol, double priceFloor, double priceCeiling);
    +
    +  Page<TestEntity> findByAction(String action, Pageable pageable);
    +
    +  Slice<TestEntity> findBySymbol(String symbol, Pageable pageable);
    +
    +  List<TestEntity> findBySymbol(String symbol, Sort sort);
    +}

    In the example above the query methods in TradeRepository are generated based on the name of the methods using thehttps://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation[Spring Data Query creation naming convention].

    Cloud Datastore only supports filter components joined by AND, and the following operations:

    • equals
    • greater than or equals
    • greater than
    • less than or equals
    • less than
    • is null

    After writing a custom repository interface specifying just the signatures of these methods, implementations are generated for you and can be used with an auto-wired instance of the repository. +Because of Cloud Datastore’s requirement that explicitly selected fields must all appear in a composite index together, find name-based query methods are run as SELECT *.

    Delete queries are also supported. +For example, query methods such as deleteByAction or removeByAction delete entities found by findByAction. +Delete queries are executed as separate read and delete operations instead of as a single transaction because Cloud Datastore cannot query in transactions unless ancestors for queries are specified. +As a result, removeBy and deleteBy name-convention query methods cannot be used inside transactions via either performInTransaction or @Transactional annotation.

    Delete queries can have the following return types:

    • An integer type that is the number of entities deleted
    • A collection of entities that were deleted
    • 'void'

    Methods can have org.springframework.data.domain.Pageable parameter to control pagination and sorting, or org.springframework.data.domain.Sort parameter to control sorting only. +See Spring Data documentation for details.

    For returning multiple items in a repository method, we support Java collections as well as org.springframework.data.domain.Page and org.springframework.data.domain.Slice. +If a method’s return type is org.springframework.data.domain.Page, the returned object will include current page, total number of results and total number of pages.

    [Note]Note

    Methods that return Page execute an additional query to compute total number of pages. +Methods that return Slice, on the other hand, don’t execute any additional queries and therefore are much more efficient.

    164.5.2 Custom GQL query methods

    Custom GQL queries can be mapped to repository methods in one of two ways:

    • namedQueries properties file
    • using the @Query annotation

    Query methods with annotation

    Using the @Query annotation:

    The names of the tags of the GQL correspond to the @Param annotated names of the method parameters.

    public interface TraderRepository extends DatastoreRepository<Trader, String> {
    +
    +  @Query("SELECT * FROM traders WHERE name = @trader_name")
    +  List<Trader> tradersByName(@Param("trader_name") String traderName);
    +
    +  @Query("SELECT * FROM  test_entities_ci WHERE id = @id_val")
    +  TestEntity getOneTestEntity(@Param("id_val") long id);
    +}

    The following parameter types are supported:

    • com.google.cloud.Timestamp
    • com.google.cloud.datastore.Blob
    • com.google.cloud.datastore.Key
    • com.google.cloud.datastore.Cursor
    • java.lang.Boolean
    • java.lang.Double
    • java.lang.Long
    • java.lang.String
    • enum values. +These are queried as String values.

    With the exception of Cursor, array forms of each of the types are also supported.

    If you would like to obtain the count of items of a query or if there are any items returned by the query, set the count = true or exists = true properties of the @Query annotation, respectively. +The return type of the query method in these cases should be an integer type or a boolean type.

    Cloud Datastore provides provides the SELECT key FROM …​ special column for all kinds that retrieves the Key`s of each row. +Selecting this special `key column is especially useful and efficient for count and exists queries.

    You can also query for non-entity types:

    	@Query(value = "SELECT __key__ from test_entities_ci")
    +	List<Key> getKeys();
    +
    +	@Query(value = "SELECT __key__ from test_entities_ci limit 1")
    +	Key getKey();
    +
    +	@Query("SELECT id FROM test_entities_ci WHERE id <= @id_val")
    +	List<String> getIds(@Param("id_val") long id);
    +
    +	@Query("SELECT id FROM test_entities_ci WHERE id <= @id_val limit 1")
    +	String getOneId(@Param("id_val") long id);

    SpEL can be used to provide GQL parameters:

    @Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act
    +  AND price > :#{#priceRadius * -1} AND price < :#{#priceRadius * 2}")
    +List<Trade> fetchByActionNamedQuery(@Param("act") String action, @Param("priceRadius") Double r);

    Kind names can be directly written in the GQL annotations. +Kind names can also be resolved from the @Entity annotation on domain classes.

    In this case, the query should refer to table names with fully qualified class names surrounded by | characters: |fully.qualified.ClassName|. +This is useful when SpEL expressions appear in the kind name provided to the @Entity annotation. +For example:

    @Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act")
    +List<Trade> fetchByActionNamedQuery(@Param("act") String action);

    Query methods with named queries properties

    You can also specify queries with Cloud Datastore parameter tags and SpEL expressions in properties files.

    By default, the namedQueriesLocation attribute on @EnableDatastoreRepositories points to the META-INF/datastore-named-queries.properties file. +You can specify the query for a method in the properties file by providing the GQL as the value for the "interface.method" property:

    Trader.fetchByName=SELECT * FROM traders WHERE name = @tag0
    public interface TraderRepository extends DatastoreRepository<Trader, String> {
    +
    +	// This method uses the query from the properties file instead of one generated based on name.
    +	List<Trader> fetchByName(@Param("tag0") String traderName);
    +
    +}

    164.5.3 Transactions

    These transactions work very similarly to those of DatastoreOperations, but is specific to the repository’s domain type and provides repository functions instead of template functions.

    For example, this is a read-write transaction:

    @Autowired
    +DatastoreRepository myRepo;
    +
    +public String doWorkInsideTransaction() {
    +  return myRepo.performTransaction(
    +    transactionDatastoreRepo -> {
    +      // Work with the single-transaction transactionDatastoreRepo here.
    +      // This is a DatastoreRepository object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    164.5.4 Projections

    Spring Data Cloud Datastore supports projections. +You can define projection interfaces based on domain types and add query methods that return them in your repository:

    public interface TradeProjection {
    +
    +	String getAction();
    +
    +	@Value("#{target.symbol + ' ' + target.action}")
    +	String getSymbolAndAction();
    +}
    +
    +public interface TradeRepository extends DatastoreRepository<Trade, Key> {
    +
    +	List<Trade> findByTraderId(String traderId);
    +
    +	List<TradeProjection> findByAction(String action);
    +
    +	@Query("SELECT action, symbol FROM trades WHERE action = @action")
    +	List<TradeProjection> findByQuery(String action);
    +}

    Projections can be provided by name-convention-based query methods as well as by custom GQL queries. +If using custom GQL queries, you can further restrict the fields retrieved from Cloud Datastore to just those required by the projection. +However, custom select statements (those not using SELECT *) require composite indexes containing the selected fields.

    Properties of projection types defined using SpEL use the fixed name target for the underlying domain object. +As a result, accessing underlying properties take the form target.<property-name>.

    164.5.5 REST Repositories

    When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file:

    <dependency>
    +  <groupId>org.springframework.boot</groupId>
    +  <artifactId>spring-boot-starter-data-rest</artifactId>
    +</dependency>

    If you prefer to configure parameters (such as path), you can use @RepositoryRestResource annotation:

    @RepositoryRestResource(collectionResourceRel = "trades", path = "trades")
    +public interface TradeRepository extends DatastoreRepository<Trade, String[]> {
    +}

    For example, you can retrieve all Trade objects in the repository by using curl http://<server>:<port>/trades, or any specific trade via curl http://<server>:<port>/trades/<trader_id>.

    You can also write trades using curl -XPOST -H"Content-Type: application/json" -d@test.json http://<server>:<port>/trades/ where the file test.json holds the JSON representation of a Trade object.

    To delete trades, you can use curl -XDELETE http://<server>:<port>/trades/<trader_id>

    164.6 Sample

    A Simple Spring Boot Application and more advanced Sample Spring Boot Application are provided to show how to use the Spring Data Cloud Datastore starter and template.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_data_cloud_spanner.html b/Greenwich.SR5/multi/multi__spring_data_cloud_spanner.html new file mode 100644 index 00000000..6c3329e4 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_data_cloud_spanner.html @@ -0,0 +1,465 @@ + + + 163. Spring Data Cloud Spanner

    163. Spring Data Cloud Spanner

    Spring Data is an abstraction for storing and retrieving POJOs in numerous storage technologies. +Spring Cloud GCP adds Spring Data support for Google Cloud Spanner.

    Maven coordinates for this module only, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-data-spanner</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-spanner'
    +}

    We provide a Spring Boot Starter for Spring Data Spanner, with which you can leverage our recommended auto-configuration setup. +To use the starter, see the coordinates see below.

    Maven:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-data-spanner</artifactId>
    +</dependency>

    Gradle:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-spanner'
    +}

    This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Spanner libraries as well.

    163.1 Configuration

    To setup Spring Data Cloud Spanner, you have to configure the following:

    • Setup the connection details to Google Cloud Spanner.
    • Enable Spring Data Repositories (optional).

    163.1.1 Cloud Spanner settings

    You can the use Spring Boot Starter for Spring Data Spanner to autoconfigure Google Cloud Spanner in your Spring application. +It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project. +The following configuration options are available:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.spanner.instance-id

    Cloud Spanner instance to use

    Yes

     

    spring.cloud.gcp.spanner.database

    Cloud Spanner database to use

    Yes

     

    spring.cloud.gcp.spanner.project-id

    GCP project ID where the Google Cloud Spanner API is hosted, if different from the one in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.spanner.credentials.location

    OAuth2 credentials for authenticating with the +Google Cloud Spanner API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.spanner.credentials.encoded-key

    Base64-encoded OAuth2 credentials for authenticating with the +Google Cloud Spanner API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.spanner.credentials.scopes

    OAuth2 scope for Spring Cloud GCP +Cloud Spanner credentials

    No

    https://www.googleapis.com/auth/spanner.data

    spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade

    If true, then schema statements generated by SpannerSchemaUtils for tables with interleaved parent-child relationships will be "ON DELETE CASCADE". +The schema for the tables will be "ON DELETE NO ACTION" if false.

    No

    true

    spring.cloud.gcp.spanner.numRpcChannels

    Number of gRPC channels used to connect to Cloud Spanner

    No

    4 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.prefetchChunks

    Number of chunks prefetched by Cloud Spanner for read and query

    No

    4 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.minSessions

    Minimum number of sessions maintained in the session pool

    No

    0 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.maxSessions

    Maximum number of sessions session pool can have

    No

    400 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.maxIdleSessions

    Maximum number of idle sessions session pool will maintain

    No

    0 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.writeSessionsFraction

    Fraction of sessions to be kept prepared for write transactions

    No

    0.2 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.keepAliveIntervalMinutes

    How long to keep idle sessions alive

    No

    30 - Determined by Cloud Spanner client library

    163.1.2 Repository settings

    Spring Data Repositories can be configured via the @EnableSpannerRepositories annotation on your main @Configuration class. +With our Spring Boot Starter for Spring Data Cloud Spanner, @EnableSpannerRepositories is automatically added. +It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by @EnableSpannerRepositories.

    163.1.3 Autoconfiguration

    Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:

    • an instance of SpannerTemplate
    • an instance of SpannerDatabaseAdminTemplate for generating table schemas from object hierarchies and creating and deleting tables and databases
    • an instance of all user-defined repositories extending SpannerRepository, CrudRepository, PagingAndSortingRepository, when repositories are enabled
    • an instance of DatabaseClient from the Google Cloud Java Client for Spanner, for convenience and lower level API access

    163.2 Object Mapping

    Spring Data Cloud Spanner allows you to map domain POJOs to Cloud Spanner tables via annotations:

    @Table(name = "traders")
    +public class Trader {
    +
    +	@PrimaryKey
    +	@Column(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@NotMapped
    +	Double temporaryNumber;
    +}

    Spring Data Cloud Spanner will ignore any property annotated with @NotMapped. +These properties will not be written to or read from Spanner.

    163.2.1 Constructors

    Simple constructors are supported on POJOs. +The constructor arguments can be a subset of the persistent properties. +Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument. +Arguments that are not directly set to properties are not supported.

    @Table(name = "traders")
    +public class Trader {
    +	@PrimaryKey
    +	@Column(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@NotMapped
    +	Double temporaryNumber;
    +
    +	public Trader(String traderId, String firstName) {
    +	    this.traderId = traderId;
    +	    this.firstName = firstName;
    +	}
    +}

    163.2.2 Table

    The @Table annotation can provide the name of the Cloud Spanner table that stores instances of the annotated class, one per row. +This annotation is optional, and if not given, the name of the table is inferred from the class name with the first character uncapitalized.

    SpEL expressions for table names

    In some cases, you might want the @Table table name to be determined dynamically. +To do that, you can use Spring Expression Language.

    For example:

    @Table(name = "trades_#{tableNameSuffix}")
    +public class Trade {
    +	// ...
    +}

    The table name will be resolved only if the tableNameSuffix value/bean in the Spring application context is defined. +For example, if tableNameSuffix has the value "123", the table name will resolve to trades_123.

    163.2.3 Primary Keys

    For a simple table, you may only have a primary key consisting of a single column. +Even in that case, the @PrimaryKey annotation is required. +@PrimaryKey identifies the one or more ID properties corresponding to the primary key.

    Spanner has first class support for composite primary keys of multiple columns. +You have to annotate all of your POJO’s fields that the primary key consists of with @PrimaryKey as below:

    @Table(name = "trades")
    +public class Trade {
    +	@PrimaryKey(keyOrder = 2)
    +	@Column(name = "trade_id")
    +	private String tradeId;
    +
    +	@PrimaryKey(keyOrder = 1)
    +	@Column(name = "trader_id")
    +	private String traderId;
    +
    +	private String action;
    +
    +	private Double price;
    +
    +	private Double shares;
    +
    +	private String symbol;
    +}

    The keyOrder parameter of @PrimaryKey identifies the properties corresponding to the primary key columns in order, starting with 1 and increasing consecutively. +Order is important and must reflect the order defined in the Cloud Spanner schema. +In our example the DDL to create the table and its primary key is as follows:

    CREATE TABLE trades (
    +    trader_id STRING(MAX),
    +    trade_id STRING(MAX),
    +    action STRING(15),
    +    symbol STRING(10),
    +    price FLOAT64,
    +    shares FLOAT64
    +) PRIMARY KEY (trader_id, trade_id)

    Spanner does not have automatic ID generation. +For most use-cases, sequential IDs should be used with caution to avoid creating data hotspots in the system. +Read Spanner Primary Keys documentation for a better understanding of primary keys and recommended practices.

    163.2.4 Columns

    All accessible properties on POJOs are automatically recognized as a Cloud Spanner column. +Column naming is generated by the PropertyNameFieldNamingStrategy by default defined on the SpannerMappingContext bean. +The @Column annotation optionally provides a different column name than that of the property and some other settings:

    • name is the optional name of the column
    • spannerTypeMaxLength specifies for STRING and BYTES columns the maximum length. +This setting is only used when generating DDL schema statements based on domain types.
    • nullable specifies if the column is created as NOT NULL. +This setting is only used when generating DDL schema statements based on domain types.
    • spannerType is the Cloud Spanner column type you can optionally specify. +If this is not specified then a compatible column type is inferred from the Java property type.
    • spannerCommitTimestamp is a boolean specifying if this property corresponds to an auto-populated commit timestamp column. +Any value set in this property will be ignored when writing to Cloud Spanner.

    163.2.5 Embedded Objects

    If an object of type B is embedded as a property of A, then the columns of B will be saved in the same Cloud Spanner table as those of A.

    If B has primary key columns, those columns will be included in the primary key of A. B can also have embedded properties. +Embedding allows reuse of columns between multiple entities, and can be useful for implementing parent-child situations, because Cloud Spanner requires child tables to include the key columns of their parents.

    For example:

    class X {
    +  @PrimaryKey
    +  String grandParentId;
    +
    +  long age;
    +}
    +
    +class A {
    +  @PrimaryKey
    +  @Embedded
    +  X grandParent;
    +
    +  @PrimaryKey(keyOrder = 2)
    +  String parentId;
    +
    +  String value;
    +}
    +
    +@Table(name = "items")
    +class B {
    +  @PrimaryKey
    +  @Embedded
    +  A parent;
    +
    +  @PrimaryKey(keyOrder = 2)
    +  String id;
    +
    +  @Column(name = "child_value")
    +  String value;
    +}

    Entities of B can be stored in a table defined as:

    CREATE TABLE items (
    +    grandParentId STRING(MAX),
    +    parentId STRING(MAX),
    +    id STRING(MAX),
    +    value STRING(MAX),
    +    child_value STRING(MAX),
    +    age INT64
    +) PRIMARY KEY (grandParentId, parentId, id)

    Note that embedded properties' column names must all be unique.

    163.2.6 Relationships

    Spring Data Cloud Spanner supports parent-child relationships using the Cloud Spanner parent-child interleaved table mechanism. +Cloud Spanner interleaved tables enforce the one-to-many relationship and provide efficient queries and operations on entities of a single domain parent entity. +These relationships can be up to 7 levels deep. +Cloud Spanner also provides automatic cascading delete or enforces the deletion of child entities before parents.

    While one-to-one and many-to-many relationships can be implemented in Cloud Spanner and Spring Data Cloud Spanner using constructs of interleaved parent-child tables, only the parent-child relationship is natively supported. +Cloud Spanner does not support the foreign key constraint, though the parent-child key constraint enforces a similar requirement when used with interleaved tables.

    For example, the following Java entities:

    @Table(name = "Singers")
    +class Singer {
    +  @PrimaryKey
    +  long SingerId;
    +
    +  String FirstName;
    +
    +  String LastName;
    +
    +  byte[] SingerInfo;
    +
    +  @Interleaved
    +  List<Album> albums;
    +}
    +
    +@Table(name = "Albums")
    +class Album {
    +  @PrimaryKey
    +  long SingerId;
    +
    +  @PrimaryKey(keyOrder = 2)
    +  long AlbumId;
    +
    +  String AlbumTitle;
    +}

    These classes can correspond to an existing pair of interleaved tables. +The @Interleaved annotation may be applied to Collection properties and the inner type is resolved as the child entity type. +The schema needed to create them can also be generated using the SpannerSchemaUtils and executed using the SpannerDatabaseAdminTemplate:

    @Autowired
    +SpannerSchemaUtils schemaUtils;
    +
    +@Autowired
    +SpannerDatabaseAdminTemplate databaseAdmin;
    +...
    +
    +// Get the create statmenets for all tables in the table structure rooted at Singer
    +List<String> createStrings = this.schemaUtils.getCreateTableDdlStringsForInterleavedHierarchy(Singer.class);
    +
    +// Create the tables and also create the database if necessary
    +this.databaseAdmin.executeDdlStrings(createStrings, true);

    The createStrings list contains table schema statements using column names and types compatible with the provided Java type and any resolved child relationship types contained within based on the configured custom converters.

    CREATE TABLE Singers (
    +  SingerId   INT64 NOT NULL,
    +  FirstName  STRING(1024),
    +  LastName   STRING(1024),
    +  SingerInfo BYTES(MAX),
    +) PRIMARY KEY (SingerId);
    +
    +CREATE TABLE Albums (
    +  SingerId     INT64 NOT NULL,
    +  AlbumId      INT64 NOT NULL,
    +  AlbumTitle   STRING(MAX),
    +) PRIMARY KEY (SingerId, AlbumId),
    +  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

    The ON DELETE CASCADE clause indicates that Cloud Spanner will delete all Albums of a singer if the Singer is deleted. +The alternative is ON DELETE NO ACTION, where a Singer cannot be deleted until all of its Albums have already been deleted. +When using SpannerSchemaUtils to generate the schema strings, the spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade boolean setting determines if these schema are generated as ON DELETE CASCADE for true and ON DELETE NO ACTION for false.

    Cloud Spanner restricts these relationships to 7 child layers. +A table may have multiple child tables.

    On updating or inserting an object to Cloud Spanner, all of its referenced children objects are also updated or inserted in the same request, respectively. +On read, all of the interleaved child rows are also all read.

    163.2.7 Supported Types

    Spring Data Cloud Spanner natively supports the following types for regular fields but also utilizes custom converters (detailed in following sections) and dozens of pre-defined Spring Data custom converters to handle other common Java types.

    Natively supported types:

    • com.google.cloud.ByteArray
    • com.google.cloud.Date
    • com.google.cloud.Timestamp
    • java.lang.Boolean, boolean
    • java.lang.Double, double
    • java.lang.Long, long
    • java.lang.Integer, int
    • java.lang.String
    • double[]
    • long[]
    • boolean[]
    • java.util.Date
    • java.util.Instant
    • java.sql.Date

    163.2.8 Lists

    Spanner supports ARRAY types for columns. +ARRAY columns are mapped to List fields in POJOS.

    Example:

    List<Double> curve;

    The types inside the lists can be any singular property type.

    163.2.9 Lists of Structs

    Cloud Spanner queries can construct STRUCT values that appear as columns in the result. +Cloud Spanner requires STRUCT values appear in ARRAYs at the root level: SELECT ARRAY(SELECT STRUCT(1 as val1, 2 as val2)) as pair FROM Users.

    Spring Data Cloud Spanner will attempt to read the column STRUCT values into a property that is an Iterable of an entity type compatible with the schema of the column STRUCT value.

    For the previous array-select example, the following property can be mapped with the constructed ARRAY<STRUCT> column: List<TwoInts> pair; where the TwoInts type is defined:

    class TwoInts {
    +
    +  int val1;
    +
    +  int val2;
    +}

    163.2.10 Custom types

    Custom converters can be used to extend the type support for user defined types.

    1. Converters need to implement the org.springframework.core.convert.converter.Converter interface in both directions.
    2. The user defined type needs to be mapped to one of the basic types supported by Spanner:

      • com.google.cloud.ByteArray
      • com.google.cloud.Date
      • com.google.cloud.Timestamp
      • java.lang.Boolean, boolean
      • java.lang.Double, double
      • java.lang.Long, long
      • java.lang.String
      • double[]
      • long[]
      • boolean[]
      • enum types
    3. An instance of both Converters needs to be passed to a ConverterAwareMappingSpannerEntityProcessor, which then has to be made available as a @Bean for SpannerEntityProcessor.

    For example:

    We would like to have a field of type Person on our Trade POJO:

    @Table(name = "trades")
    +public class Trade {
    +  //...
    +  Person person;
    +  //...
    +}

    Where Person is a simple class:

    public class Person {
    +
    +  public String firstName;
    +  public String lastName;
    +
    +}

    We have to define the two converters:

      public class PersonWriteConverter implements Converter<Person, String> {
    +
    +    @Override
    +    public String convert(Person person) {
    +      return person.firstName + " " + person.lastName;
    +    }
    +  }
    +
    +  public class PersonReadConverter implements Converter<String, Person> {
    +
    +    @Override
    +    public Person convert(String s) {
    +      Person person = new Person();
    +      person.firstName = s.split(" ")[0];
    +      person.lastName = s.split(" ")[1];
    +      return person;
    +    }
    +  }

    That will be configured in our @Configuration file:

    @Configuration
    +public class ConverterConfiguration {
    +
    +	@Bean
    +	public SpannerEntityProcessor spannerEntityProcessor(SpannerMappingContext spannerMappingContext) {
    +		return new ConverterAwareMappingSpannerEntityProcessor(spannerMappingContext,
    +				Arrays.asList(new PersonWriteConverter()),
    +				Arrays.asList(new PersonReadConverter()));
    +	}
    +}

    163.2.11 Custom Converter for Struct Array Columns

    If a Converter<Struct, A> is provided, then properties of type List<A> can be used in your entity types.

    163.3 Spanner Operations & Template

    SpannerOperations and its implementation, SpannerTemplate, provides the Template pattern familiar to Spring developers. +It provides:

    • Resource management
    • One-stop-shop to Spanner operations with the Spring Data POJO mapping and conversion features
    • Exception conversion

    Using the autoconfigure provided by our Spring Boot Starter for Spanner, your Spring application context will contain a fully configured SpannerTemplate object that you can easily autowire in your application:

    @SpringBootApplication
    +public class SpannerTemplateExample {
    +
    +	@Autowired
    +	SpannerTemplate spannerTemplate;
    +
    +	public void doSomething() {
    +		this.spannerTemplate.delete(Trade.class, KeySet.all());
    +		//...
    +		Trade t = new Trade();
    +		//...
    +		this.spannerTemplate.insert(t);
    +		//...
    +		List<Trade> tradesByAction = spannerTemplate.findAll(Trade.class);
    +		//...
    +	}
    +}

    The Template API provides convenience methods for:

    • Reads, and by providing SpannerReadOptions and +SpannerQueryOptions

      • Stale read
      • Read with secondary indices
      • Read with limits and offsets
      • Read with sorting
    • Queries
    • DML operations (delete, insert, update, upsert)
    • Partial reads

      • You can define a set of columns to be read into your entity
    • Partial writes

      • Persist only a few properties from your entity
    • Read-only transactions
    • Locking read-write transactions

    163.3.1 SQL Query

    Cloud Spanner has SQL support for running read-only queries. +All the query related methods start with query on SpannerTemplate. +Using SpannerTemplate you can execute SQL queries that map to POJOs:

    List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"));

    163.3.2 Read

    Spanner exposes a Read API for reading single row or multiple rows in a table or in a secondary index.

    Using SpannerTemplate you can execute reads, for example:

    List<Trade> trades = this.spannerTemplate.readAll(Trade.class);

    Main benefit of reads over queries is reading multiple rows of a certain pattern of keys is much easier using the features of the KeySet class.

    163.3.3 Advanced reads

    Stale read

    All reads and queries are strong reads by default. +A strong read is a read at a current timestamp and is guaranteed to see all data that has been committed up until the start of this read. +A stale read on the other hand is read at a timestamp in the past. +Cloud Spanner allows you to determine how current the data should be when you read data. +With SpannerTemplate you can specify the Timestamp by setting it on SpannerQueryOptions or SpannerReadOptions to the appropriate read or query methods:

    Reads:

    // a read with options:
    +SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setTimestamp(Timestamp.now());
    +List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);

    Queries:

    // a query with options:
    +SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setTimestamp(Timestamp.now());
    +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);

    Read from a secondary index

    Using a secondary index is available for Reads via the Template API and it is also implicitly available via SQL for Queries.

    The following shows how to read rows from a table using a secondary index simply by setting index on SpannerReadOptions:

    SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setIndex("TradesByTrader");
    +List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);

    Read with offsets and limits

    Limits and offsets are only supported by Queries. +The following will get only the first two rows of the query:

    SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setLimit(2).setOffset(3);
    +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);

    Note that the above is equivalent of executing SELECT * FROM trades LIMIT 2 OFFSET 3.

    Sorting

    Reads by keys do not support sorting. +However, queries on the Template API support sorting through standard SQL and also via Spring Data Sort API:

    List<Trade> trades = this.spannerTemplate.queryAll(Trade.class, Sort.by("action"));

    If the provided sorted field name is that of a property of the domain type, then the column name corresponding to that property will be used in the query. +Otherwise, the given field name is assumed to be the name of the column in the Cloud Spanner table. +Sorting on columns of Cloud Spanner types STRING and BYTES can be done while ignoring case:

    Sort.by(Order.desc("action").ignoreCase())

    Partial read

    Partial read is only possible when using Queries. +In case the rows returned by the query have fewer columns than the entity that it will be mapped to, Spring Data will map the returned columns only. +This setting also applies to nested structs and their corresponding nested POJO properties.

    List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT action, symbol FROM trades"),
    +    new SpannerQueryOptions().setAllowMissingResultSetColumns(true));

    If the setting is set to false, then an exception will be thrown if there are missing columns in the query result.

    Summary of options for Query vs Read

    Feature

    Query supports it

    Read supports it

    SQL

    yes

    no

    Partial read

    yes

    no

    Limits

    yes

    no

    Offsets

    yes

    no

    Secondary index

    yes

    yes

    Read using index range

    no

    yes

    Sorting

    yes

    no

    163.3.4 Write / Update

    The write methods of SpannerOperations accept a POJO and writes all of its properties to Spanner. +The corresponding Spanner table and entity metadata is obtained from the given object’s actual type.

    If a POJO was retrieved from Spanner and its primary key properties values were changed and then written or updated, the operation will occur as if against a row with the new primary key values. +The row with the original primary key values will not be affected.

    Insert

    The insert method of SpannerOperations accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if a row with the POJO’s primary key already exists in the table.

    Trade t = new Trade();
    +this.spannerTemplate.insert(t);

    Update

    The update method of SpannerOperations accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if the POJO’s primary key does not already exist in the table.

    // t was retrieved from a previous operation
    +this.spannerTemplate.update(t);

    Upsert

    The upsert method of SpannerOperations accepts a POJO and writes all of its properties to Spanner using update-or-insert.

    // t was retrieved from a previous operation or it's new
    +this.spannerTemplate.upsert(t);

    Partial Update

    The update methods of SpannerOperations operate by default on all properties within the given object, but also accept String[] and Optional<Set<String>> of column names. +If the Optional of set of column names is empty, then all columns are written to Spanner. +However, if the Optional is occupied by an empty set, then no columns will be written.

    // t was retrieved from a previous operation or it's new
    +this.spannerTemplate.update(t, "symbol", "action");

    163.3.5 DML

    DML statements can be executed using SpannerOperations.executeDmlStatement. +Inserts, updates, and deletions can affect any number of rows and entities.

    163.3.6 Transactions

    SpannerOperations provides methods to run java.util.Function objects within a single transaction while making available the read and write methods from SpannerOperations.

    Read/Write Transaction

    Read and write transactions are provided by SpannerOperations via the performReadWriteTransaction method:

    @Autowired
    +SpannerOperations mySpannerOperations;
    +
    +public String doWorkInsideTransaction() {
    +  return mySpannerOperations.performReadWriteTransaction(
    +    transActionSpannerOperations -> {
    +      // Work with transActionSpannerOperations here.
    +      // It is also a SpannerOperations object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    The performReadWriteTransaction method accepts a Function that is provided an instance of a SpannerOperations object. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular SpannerOperations with a few exceptions:

    • Its read functionality cannot perform stale reads, because all reads and writes happen at the single point in time of the transaction.
    • It cannot perform sub-transactions via performReadWriteTransaction or performReadOnlyTransaction.

    As these read-write transactions are locking, it is recommended that you use the performReadOnlyTransaction if your function does not perform any writes.

    Read-only Transaction

    The performReadOnlyTransaction method is used to perform read-only transactions using a SpannerOperations:

    @Autowired
    +SpannerOperations mySpannerOperations;
    +
    +public String doWorkInsideTransaction() {
    +  return mySpannerOperations.performReadOnlyTransaction(
    +    transActionSpannerOperations -> {
    +      // Work with transActionSpannerOperations here.
    +      // It is also a SpannerOperations object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    The performReadOnlyTransaction method accepts a Function that is provided an instance of a +SpannerOperations object. +This method also accepts a ReadOptions object, but the only attribute used is the timestamp used to determine the snapshot in time to perform the reads in the transaction. +If the timestamp is not set in the read options the transaction is run against the current state of the database. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular SpannerOperations with +a few exceptions:

    • Its read functionality cannot perform stale reads, because all reads happen at the single point in time of the transaction.
    • It cannot perform sub-transactions via performReadWriteTransaction or performReadOnlyTransaction
    • It cannot perform any write operations.

    Because read-only transactions are non-locking and can be performed on points in time in the past, these are recommended for functions that do not perform write operations.

    Declarative Transactions with @Transactional Annotation

    This feature requires a bean of SpannerTransactionManager, which is provided when using spring-cloud-gcp-starter-data-spanner.

    SpannerTemplate and SpannerRepository support running methods with the @Transactional [annotation](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative) as transactions. +If a method annotated with @Transactional calls another method also annotated, then both methods will work within the same transaction. +performReadOnlyTransaction and performReadWriteTransaction cannot be used in @Transactional annotated methods because Cloud Spanner does not support transactions within transactions.

    163.3.7 DML Statements

    SpannerTemplate supports [DML](https://cloud.google.com/spanner/docs/dml-tasks) Statements. +DML statements can be executed in transactions via performReadWriteTransaction or using the @Transactional annotation.

    When DML statements are executed outside of transactions, they are executed in [partitioned-mode](https://cloud.google.com/spanner/docs/dml-tasks#partitioned-dml).

    163.4 Repositories

    Spring Data Repositories are a powerful abstraction that can save you a lot of boilerplate code.

    For example:

    public interface TraderRepository extends SpannerRepository<Trader, String> {
    +}

    Spring Data generates a working implementation of the specified interface, which can be conveniently autowired into an application.

    The Trader type parameter to SpannerRepository refers to the underlying domain type. +The second type parameter, String in this case, refers to the type of the key of the domain type.

    For POJOs with a composite primary key, this ID type parameter can be any descendant of Object[] compatible with all primary key properties, any descendant of Iterable, or com.google.cloud.spanner.Key. +If the domain POJO type only has a single primary key column, then the primary key property type can be used or the Key type.

    For example in case of Trades, that belong to a Trader, TradeRepository would look like this:

    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +
    +}
    public class MyApplication {
    +
    +	@Autowired
    +	SpannerTemplate spannerTemplate;
    +
    +	@Autowired
    +	StudentRepository studentRepository;
    +
    +	public void demo() {
    +
    +		this.tradeRepository.deleteAll();
    +		String traderId = "demo_trader";
    +		Trade t = new Trade();
    +		t.symbol = stock;
    +		t.action = action;
    +		t.traderId = traderId;
    +		t.price = 100.0;
    +		t.shares = 12345.6;
    +		this.spannerTemplate.insert(t);
    +
    +		Iterable<Trade> allTrades = this.tradeRepository.findAll();
    +
    +		int count = this.tradeRepository.countByAction("BUY");
    +
    +	}
    +}

    163.4.1 CRUD Repository

    CrudRepository methods work as expected, with one thing Spanner specific: the save and saveAll methods work as update-or-insert.

    163.4.2 Paging and Sorting Repository

    You can also use PagingAndSortingRepository with Spanner Spring Data. +The sorting and pageable findAll methods available from this interface operate on the current state of the Spanner database. +As a result, beware that the state of the database (and the results) might change when moving page to page.

    163.4.3 Spanner Repository

    The SpannerRepository extends the PagingAndSortingRepository, but adds the read-only and the read-write transaction functionality provided by Spanner. +These transactions work very similarly to those of SpannerOperations, but is specific to the repository’s domain type and provides repository functions instead of template functions.

    For example, this is a read-write transaction:

    @Autowired
    +SpannerRepository myRepo;
    +
    +public String doWorkInsideTransaction() {
    +  return myRepo.performReadOnlyTransaction(
    +    transactionSpannerRepo -> {
    +      // Work with the single-transaction transactionSpannerRepo here.
    +      // This is a SpannerRepository object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    When creating custom repositories for your own domain types and query methods, you can extend SpannerRepository to access Cloud Spanner-specific features as well as all features from PagingAndSortingRepository and CrudRepository.

    163.5 Query Methods

    SpannerRepository supports Query Methods. +Described in the following sections, these are methods residing in your custom repository interfaces of which implementations are generated based on their names and annotations. +Query Methods can read, write, and delete entities in Cloud Spanner. +Parameters to these methods can be any Cloud Spanner data type supported directly or via custom configured converters. +Parameters can also be of type Struct or POJOs. +If a POJO is given as a parameter, it will be converted to a Struct with the same type-conversion logic as used to create write mutations. +Comparisons using Struct parameters are limited to what is available with Cloud Spanner.

    163.5.1 Query methods by convention

    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +    List<Trade> findByAction(String action);
    +
    +	int countByAction(String action);
    +
    +	// Named methods are powerful, but can get unwieldy
    +	List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(
    +  			String action, String symbol, String traderId);
    +}

    In the example above, the query methods in TradeRepository are generated based on the name of the methods, using the Spring Data Query creation naming convention.

    List<Trade> findByAction(String action) would translate to a SELECT * FROM trades WHERE action = ?.

    The function List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(String action, String symbol, String traderId); will be translated as the equivalent of this SQL query:

    SELECT DISTINCT * FROM trades
    +WHERE ACTION = ? AND LOWER(SYMBOL) = LOWER(?) AND TRADER_ID = ?
    +ORDER BY SYMBOL DESC
    +LIMIT 3

    The following filter options are supported:

    • Equality
    • Greater than or equals
    • Greater than
    • Less than or equals
    • Less than
    • Is null
    • Is not null
    • Is true
    • Is false
    • Like a string
    • Not like a string
    • Contains a string
    • Not contains a string

    Note that the phrase SymbolIgnoreCase is translated to LOWER(SYMBOL) = LOWER(?) indicating a non-case-sensitive matching. +The IgnoreCase phrase may only be appended to fields that correspond to columns of type STRING or BYTES. +The Spring Data "AllIgnoreCase" phrase appended at the end of the method name is not supported.

    The Like or NotLike naming conventions:

    List<Trade> findBySymbolLike(String symbolFragment);

    The param symbolFragment can contain wildcard characters for string matching such as _ and %.

    The Contains and NotContains naming conventions:

    List<Trade> findBySymbolContains(String symbolFragment);

    The param symbolFragment is a regular expression that is checked for occurrences.

    Delete queries are also supported. +For example, query methods such as deleteByAction or removeByAction delete entities found by findByAction. +The delete operation happens in a single transaction.

    Delete queries can have the following return types: +* An integer type that is the number of entities deleted +* A collection of entities that were deleted +* void

    163.5.2 Custom SQL/DML query methods

    The example above for List<Trade> fetchByActionNamedQuery(String action) does not match the Spring Data Query creation naming convention, so we have to map a parametrized Spanner SQL query to it.

    The SQL query for the method can be mapped to repository methods in one of two ways:

    • namedQueries properties file
    • using the @Query annotation

    The names of the tags of the SQL correspond to the @Param annotated names of the method parameters.

    Custom SQL query methods can accept a single Sort or Pageable parameter that is applied on top of any sorting or paging in the SQL:

    	@Query("SELECT * FROM trades ORDER BY action DESC")
    +	List<Trade> sortedTrades(Pageable pageable);
    +
    +	@Query("SELECT * FROM trades ORDER BY action DESC LIMIT 1")
    + 	Trade sortedTopTrade(Pageable pageable);

    This can be used:

    	List<Trade> customSortedTrades = tradeRepository.sortedTrades(PageRequest
    +  				.of(2, 2, org.springframework.data.domain.Sort.by(Order.asc("id"))));

    The results would be sorted by "id" in ascending order.

    Your query method can also return non-entity types:

      	@Query("SELECT COUNT(1) FROM trades WHERE action = @action")
    +  	int countByActionQuery(String action);
    +
    +  	@Query("SELECT EXISTS(SELECT COUNT(1) FROM trades WHERE action = @action)")
    +  	boolean existsByActionQuery(String action);
    +
    +  	@Query("SELECT action FROM trades WHERE action = @action LIMIT 1")
    +  	String getFirstString(@Param("action") String action);
    +
    +  	@Query("SELECT action FROM trades WHERE action = @action")
    +  	List<String> getFirstStringList(@Param("action") String action);

    DML statements can also be executed by query methods, but the only possible return value is a long representing the number of affected rows. +The dmlStatement boolean setting must be set on @Query to indicate that the query method is executed as a DML statement.

      	@Query(value = "DELETE FROM trades WHERE action = @action", dmlStatement = true)
    +  	long deleteByActionQuery(String action);

    Query methods with named queries properties

    By default, the namedQueriesLocation attribute on @EnableSpannerRepositories points to the META-INF/spanner-named-queries.properties file. +You can specify the query for a method in the properties file by providing the SQL as the value for the "interface.method" property:

    Trade.fetchByActionNamedQuery=SELECT * FROM trades WHERE trades.action = @tag0
    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +	// This method uses the query from the properties file instead of one generated based on name.
    +	List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
    +}

    Query methods with annotation

    Using the @Query annotation:

    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +    @Query("SELECT * FROM trades WHERE trades.action = @tag0")
    +    List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
    +}

    Table names can be used directly. +For example, "trades" in the above example. +Alternatively, table names can be resolved from the @Table annotation on domain classes as well. +In this case, the query should refer to table names with fully qualified class names between : +characters: :fully.qualified.ClassName:. +A full example would look like:

    @Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0")
    +List<Trade> fetchByActionNamedQuery(String action);

    This allows table names evaluated with SpEL to be used in custom queries.

    SpEL can also be used to provide SQL parameters:

    @Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0
    +  AND price > #{#priceRadius * -1} AND price < #{#priceRadius * 2}")
    +List<Trade> fetchByActionNamedQuery(String action, Double priceRadius);

    163.5.3 Projections

    Spring Data Spanner supports projections. +You can define projection interfaces based on domain types and add query methods that return them in your repository:

    public interface TradeProjection {
    +
    +	String getAction();
    +
    +	@Value("#{target.symbol + ' ' + target.action}")
    +	String getSymbolAndAction();
    +}
    +
    +public interface TradeRepository extends SpannerRepository<Trade, Key> {
    +
    +	List<Trade> findByTraderId(String traderId);
    +
    +	List<TradeProjection> findByAction(String action);
    +
    +	@Query("SELECT action, symbol FROM trades WHERE action = @action")
    +	List<TradeProjection> findByQuery(String action);
    +}

    Projections can be provided by name-convention-based query methods as well as by custom SQL queries. +If using custom SQL queries, you can further restrict the columns retrieved from Spanner to just those required by the projection to improve performance.

    Properties of projection types defined using SpEL use the fixed name target for the underlying domain object. +As a result accessing underlying properties take the form target.<property-name>.

    163.5.4 REST Repositories

    When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file:

    <dependency>
    +  <groupId>org.springframework.boot</groupId>
    +  <artifactId>spring-boot-starter-data-rest</artifactId>
    +</dependency>

    If you prefer to configure parameters (such as path), you can use @RepositoryRestResource annotation:

    @RepositoryRestResource(collectionResourceRel = "trades", path = "trades")
    +public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +}

    For example, you can retrieve all Trade objects in the repository by using curl http://<server>:<port>/trades, or any specific trade via curl http://<server>:<port>/trades/<trader_id>,<trade_id>.

    The separator between your primary key components, id and trader_id in this case, is a comma by default, but can be configured to any string not found in your key values by extending the SpannerKeyIdConverter class:

    @Component
    +class MySpecialIdConverter extends SpannerKeyIdConverter {
    +
    +    @Override
    +    protected String getUrlIdSeparator() {
    +        return ":";
    +    }
    +}

    You can also write trades using curl -XPOST -H"Content-Type: application/json" -d@test.json http://<server>:<port>/trades/ where the file test.json holds the JSON representation of a Trade object.

    163.6 Database and Schema Admin

    Databases and tables inside Spanner instances can be created automatically from SpannerPersistentEntity objects:

    @Autowired
    +private SpannerSchemaUtils spannerSchemaUtils;
    +
    +@Autowired
    +private SpannerDatabaseAdminTemplate spannerDatabaseAdminTemplate;
    +
    +public void createTable(SpannerPersistentEntity entity) {
    +	if(!spannerDatabaseAdminTemplate.tableExists(entity.tableName()){
    +
    +	  // The boolean parameter indicates that the database will be created if it does not exist.
    +	  spannerDatabaseAdminTemplate.executeDdlStrings(Arrays.asList(
    +            spannerSchemaUtils.getCreateTableDDLString(entity.getType())), true);
    +	}
    +}

    Schemas can be generated for entire object hierarchies with interleaved relationships and composite keys.

    163.7 Sample

    A sample application is available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_integration.html b/Greenwich.SR5/multi/multi__spring_integration.html new file mode 100644 index 00000000..9fe466ce --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_integration.html @@ -0,0 +1,113 @@ + + + 158. Spring Integration

    158. Spring Integration

    Spring Cloud GCP provides Spring Integration adapters that allow your applications to use Enterprise Integration Patterns backed up by Google Cloud Platform services.

    158.1 Channel Adapters for Cloud Pub/Sub

    The channel adapters for Google Cloud Pub/Sub connect your Spring MessageChannels to Google Cloud Pub/Sub topics and subscriptions. +This enables messaging between different processes, applications or micro-services backed up by Google Cloud Pub/Sub.

    The Spring Integration Channel Adapters for Google Cloud Pub/Sub are included in the spring-cloud-gcp-pubsub module.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-pubsub</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.integration</groupId>
    +    <artifactId>spring-integration-core</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub'
    +    compile group: 'org.springframework.integration', name: 'spring-integration-core'
    +}

    158.1.1 Inbound channel adapter

    PubSubInboundChannelAdapter is the inbound channel adapter for GCP Pub/Sub that listens to a GCP Pub/Sub subscription for new messages. +It converts new messages to an internal Spring Message and then sends it to the bound output channel.

    Google Pub/Sub treats message payloads as byte arrays. +So, by default, the inbound channel adapter will construct the Spring Message with byte[] as the payload. +However, you can change the desired payload type by setting the payloadType property of the PubSubInboundChannelAdapter. +The PubSubInboundChannelAdapter delegates the conversion to the desired payload type to the PubSubMessageConverter configured in the PubSubTemplate.

    To use the inbound channel adapter, a PubSubInboundChannelAdapter must be provided and configured on the user application side.

    @Bean
    +public MessageChannel pubsubInputChannel() {
    +    return new PublishSubscribeChannel();
    +}
    +
    +@Bean
    +public PubSubInboundChannelAdapter messageChannelAdapter(
    +    @Qualifier("pubsubInputChannel") MessageChannel inputChannel,
    +    SubscriberFactory subscriberFactory) {
    +    PubSubInboundChannelAdapter adapter =
    +        new PubSubInboundChannelAdapter(subscriberFactory, "subscriptionName");
    +    adapter.setOutputChannel(inputChannel);
    +    adapter.setAckMode(AckMode.MANUAL);
    +
    +    return adapter;
    +}

    In the example, we first specify the MessageChannel where the adapter is going to write incoming messages to. +The MessageChannel implementation isn’t important here. +Depending on your use case, you might want to use a MessageChannel other than PublishSubscribeChannel.

    Then, we declare a PubSubInboundChannelAdapter bean. +It requires the channel we just created and a SubscriberFactory, which creates Subscriber objects from the Google Cloud Java Client for Pub/Sub. +The Spring Boot starter for GCP Pub/Sub provides a configured SubscriberFactory.

    The PubSubInboundChannelAdapter supports three acknowledgement modes, with AckMode.AUTO being the default value;

    Automatic acking (AckMode.AUTO)

    A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown. +If a RuntimeException is thrown while the message is processed, then the message is nacked.

    Automatic acking OK (AckMode.AUTO_ACK)

    A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown. +If a RuntimeException is thrown while the message is processed, then the message is neither acked / nor nacked.

    This is useful when using the subscription’s ack deadline timeout as a retry delivery backoff mechanism.

    Manually acking (AckMode.MANUAL)

    The adapter attaches a BasicAcknowledgeablePubsubMessage object to the Message headers. +Users can extract the BasicAcknowledgeablePubsubMessage using the GcpPubSubHeaders.ORIGINAL_MESSAGE key and use it to (n)ack a message.

    @Bean
    +@ServiceActivator(inputChannel = "pubsubInputChannel")
    +public MessageHandler messageReceiver() {
    +    return message -> {
    +        LOGGER.info("Message arrived! Payload: " + new String((byte[]) message.getPayload()));
    +        BasicAcknowledgeablePubsubMessage originalMessage =
    +              message.getHeaders().get(GcpPubSubHeaders.ORIGINAL_MESSAGE, BasicAcknowledgeablePubsubMessage.class);
    +        originalMessage.ack();
    +    };
    +}

    158.1.2 Outbound channel adapter

    PubSubMessageHandler is the outbound channel adapter for GCP Pub/Sub that listens for new messages on a Spring MessageChannel. +It uses PubSubTemplate to post them to a GCP Pub/Sub topic.

    To construct a Pub/Sub representation of the message, the outbound channel adapter needs to convert the Spring Message payload to a byte array representation expected by Pub/Sub. +It delegates this conversion to the PubSubTemplate. +To customize the conversion, you can specify a PubSubMessageConverter in the PubSubTemplate that should convert the Object payload and headers of the Spring Message to a PubsubMessage.

    To use the outbound channel adapter, a PubSubMessageHandler bean must be provided and configured on the user application side.

    @Bean
    +@ServiceActivator(inputChannel = "pubsubOutputChannel")
    +public MessageHandler messageSender(PubSubTemplate pubsubTemplate) {
    +    return new PubSubMessageHandler(pubsubTemplate, "topicName");
    +}

    The provided PubSubTemplate contains all the necessary configuration to publish messages to a GCP Pub/Sub topic.

    PubSubMessageHandler publishes messages asynchronously by default. +A publish timeout can be configured for synchronous publishing. +If none is provided, the adapter waits indefinitely for a response.

    It is possible to set user-defined callbacks for the publish() call in PubSubMessageHandler through the setPublishFutureCallback() method. +These are useful to process the message ID, in case of success, or the error if any was thrown.

    To override the default destination you can use the GcpPubSubHeaders.DESTINATION header.

    @Autowired
    +private MessageChannel pubsubOutputChannel;
    +
    +public void handleMessage(Message<?> msg) throws MessagingException {
    +    final Message<?> message = MessageBuilder
    +        .withPayload(msg.getPayload())
    +        .setHeader(GcpPubSubHeaders.TOPIC, "customTopic").build();
    +    pubsubOutputChannel.send(message);
    +}

    It is also possible to set an SpEL expression for the topic with the setTopicExpression() or setTopicExpressionString() methods.

    158.1.3 Header mapping

    These channel adapters contain header mappers that allow you to map, or filter out, headers from Spring to Google Cloud Pub/Sub messages, and vice-versa. +By default, the inbound channel adapter maps every header on the Google Cloud Pub/Sub messages to the Spring messages produced by the adapter. +The outbound channel adapter maps every header from Spring messages into Google Cloud Pub/Sub ones, except the ones added by Spring, like headers with key "id", "timestamp" and "gcp_pubsub_acknowledgement". +In the process, the outbound mapper also converts the value of the headers into string.

    Each adapter declares a setHeaderMapper() method to let you further customize which headers you want to map from Spring to Google Cloud Pub/Sub, and vice-versa.

    For example, to filter out headers "foo", "bar" and all headers starting with the prefix "prefix_", you can use setHeaderMapper() along with the PubSubHeaderMapper implementation provided by this module.

    PubSubMessageHandler adapter = ...
    +...
    +PubSubHeaderMapper headerMapper = new PubSubHeaderMapper();
    +headerMapper.setOutboundHeaderPatterns("!foo", "!bar", "!prefix_*", "*");
    +adapter.setHeaderMapper(headerMapper);
    [Note]Note

    The order in which the patterns are declared in PubSubHeaderMapper.setOutboundHeaderPatterns() and PubSubHeaderMapper.setInboundHeaderPatterns() matters. +The first patterns have precedence over the following ones.

    In the previous example, the "*" pattern means every header is mapped. +However, because it comes last in the list, the previous patterns take precedence.

    158.3 Channel Adapters for Google Cloud Storage

    The channel adapters for Google Cloud Storage allow you to read and write files to Google Cloud Storage through MessageChannels.

    Spring Cloud GCP provides two inbound adapters, GcsInboundFileSynchronizingMessageSource and GcsStreamingMessageSource, and one outbound adapter, GcsMessageHandler.

    The Spring Integration Channel Adapters for Google Cloud Storage are included in the spring-cloud-gcp-storage module.

    To use the Storage portion of Spring Integration for Spring Cloud GCP, you must also provide the spring-integration-file dependency, since it isn’t pulled transitively.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-storage</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.integration</groupId>
    +    <artifactId>spring-integration-file</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage'
    +    compile group: 'org.springframework.integration', name: 'spring-integration-file'
    +}

    158.3.1 Inbound channel adapter

    The Google Cloud Storage inbound channel adapter polls a Google Cloud Storage bucket for new files and sends each of them in a Message payload to the MessageChannel specified in the @InboundChannelAdapter annotation. +The files are temporarily stored in a folder in the local file system.

    Here is an example of how to configure a Google Cloud Storage inbound channel adapter.

    @Bean
    +@InboundChannelAdapter(channel = "new-file-channel", poller = @Poller(fixedDelay = "5000"))
    +public MessageSource<File> synchronizerAdapter(Storage gcs) {
    +  GcsInboundFileSynchronizer synchronizer = new GcsInboundFileSynchronizer(gcs);
    +  synchronizer.setRemoteDirectory("your-gcs-bucket");
    +
    +  GcsInboundFileSynchronizingMessageSource synchAdapter =
    +          new GcsInboundFileSynchronizingMessageSource(synchronizer);
    +  synchAdapter.setLocalDirectory(new File("local-directory"));
    +
    +  return synchAdapter;
    +}

    158.3.2 Inbound streaming channel adapter

    The inbound streaming channel adapter is similar to the normal inbound channel adapter, except it does not require files to be stored in the file system.

    Here is an example of how to configure a Google Cloud Storage inbound streaming channel adapter.

    @Bean
    +@InboundChannelAdapter(channel = "streaming-channel", poller = @Poller(fixedDelay = "5000"))
    +public MessageSource<InputStream> streamingAdapter(Storage gcs) {
    +  GcsStreamingMessageSource adapter =
    +          new GcsStreamingMessageSource(new GcsRemoteFileTemplate(new GcsSessionFactory(gcs)));
    +  adapter.setRemoteDirectory("your-gcs-bucket");
    +  return adapter;
    +}

    158.3.3 Outbound channel adapter

    The outbound channel adapter allows files to be written to Google Cloud Storage. +When it receives a Message containing a payload of type File, it writes that file to the Google Cloud Storage bucket specified in the adapter.

    Here is an example of how to configure a Google Cloud Storage outbound channel adapter.

    @Bean
    +@ServiceActivator(inputChannel = "writeFiles")
    +public MessageHandler outboundChannelAdapter(Storage gcs) {
    +  GcsMessageHandler outboundChannelAdapter = new GcsMessageHandler(new GcsSessionFactory(gcs));
    +  outboundChannelAdapter.setRemoteDirectoryExpression(new ValueExpression<>("your-gcs-bucket"));
    +
    +  return outboundChannelAdapter;
    +}

    158.4 Sample

    A sample application is available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_jdbc.html b/Greenwich.SR5/multi/multi__spring_jdbc.html new file mode 100644 index 00000000..b443f697 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_jdbc.html @@ -0,0 +1,42 @@ + + + 157. Spring JDBC

    157. Spring JDBC

    Spring Cloud GCP adds integrations with +Spring JDBC so you can run your MySQL or PostgreSQL databases in Google Cloud SQL using Spring JDBC, or other libraries that depend on it like Spring Data JPA.

    The Cloud SQL support is provided by Spring Cloud GCP in the form of two Spring Boot starters, one for MySQL and another one for PostgreSQL. +The role of the starters is to read configuration from properties and assume default settings so that user experience connecting to MySQL and PostgreSQL is as simple as possible.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-sql-postgresql</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-mysql'
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-postgresql'
    +}

    157.1 Prerequisites

    In order to use the Spring Boot Starters for Google Cloud SQL, the Google Cloud SQL API must be enabled in your GCP project.

    To do that, go to the API library page of the Google Cloud Console, search for "Cloud SQL API", click the first result and enable the API.

    [Note]Note

    There are several similar "Cloud SQL" results. +You must access the "Google Cloud SQL API" one and enable the API from there.

    157.2 Spring Boot Starter for Google Cloud SQL

    The Spring Boot Starters for Google Cloud SQL provide an auto-configured DataSource object. +Coupled with Spring JDBC, it provides a +JdbcTemplate object bean that allows for operations such as querying and modifying a database.

    public List<Map<String, Object>> listUsers() {
    +    return jdbcTemplate.queryForList("SELECT * FROM user;");
    +}

    You can rely on +Spring Boot data source auto-configuration to configure a DataSource bean. +In other words, properties like the SQL username, spring.datasource.username, and password, spring.datasource.password can be used. +There is also some configuration specific to Google Cloud SQL:

    Property name

    Description

    Default value

    spring.cloud.gcp.sql.enabled

    Enables or disables Cloud SQL auto configuration

    true

    spring.cloud.gcp.sql.database-name

    Name of the database to connect to.

     

    spring.cloud.gcp.sql.instance-connection-name

    A string containing a Google Cloud SQL instance’s project ID, region and name, each separated by a colon. +For example, my-project-id:my-region:my-instance-name.

     

    spring.cloud.gcp.sql.credentials.location

    File system path to the Google OAuth2 credentials private key file. +Used to authenticate and authorize new connections to a Google Cloud SQL instance.

    Default credentials provided by the Spring GCP Boot starter

    spring.cloud.gcp.sql.credentials.encoded-key

    Base64-encoded contents of OAuth2 account private key in JSON format. +Used to authenticate and authorize new connections to a Google Cloud SQL instance.

    Default credentials provided by the Spring GCP Boot starter

    [Note]Note

    If you provide your own spring.datasource.url, it will be ignored, unless you disable Cloud SQL auto configuration with spring.cloud.gcp.sql.enabled=false.

    157.2.1 DataSource creation flow

    Based on the previous properties, the Spring Boot starter for Google Cloud SQL creates a CloudSqlJdbcInfoProvider object which is used to obtain an instance’s JDBC URL and driver class name. +If you provide your own CloudSqlJdbcInfoProvider bean, it is used instead and the properties related to building the JDBC URL or driver class are ignored.

    The DataSourceProperties object provided by Spring Boot Autoconfigure is mutated in order to use the JDBC URL and driver class names provided by CloudSqlJdbcInfoProvider, unless those values were provided in the properties. +It is in the DataSourceProperties mutation step that the credentials factory is registered in a system property to be SqlCredentialFactory.

    DataSource creation is delegated to +Spring Boot. +You can select the type of connection pool (e.g., Tomcat, HikariCP, etc.) by adding their dependency to the classpath.

    Using the created DataSource in conjunction with Spring JDBC provides you with a fully configured and operational JdbcTemplate object that you can use to interact with your SQL database. +You can connect to your database with as little as a database and instance names.

    157.2.2 Troubleshooting tips

    Connection issues

    If you’re not able to connect to a database and see an endless loop of Connecting to Cloud SQL instance […​] on IP […​], it’s likely that exceptions are being thrown and logged at a level lower than your logger’s level. +This may be the case with HikariCP, if your logger is set to INFO or higher level.

    To see what’s going on in the background, you should add a logback.xml file to your application resources folder, that looks like this:

    <?xml version="1.0" encoding="UTF-8"?>
    +<configuration>
    +  <include resource="org/springframework/boot/logging/logback/base.xml"/>
    +  <logger name="com.zaxxer.hikari.pool" level="DEBUG"/>
    +</configuration>

    Errors like c.g.cloud.sql.core.SslSocketFactory : Re-throwing cached exception due to attempt to refresh instance information too soon after error

    If you see a lot of errors like this in a loop and can’t connect to your database, this is usually a symptom that something isn’t right with the permissions of your credentials or the Google Cloud SQL API is not enabled. +Verify that the Google Cloud SQL API is enabled in the Cloud Console and that your service account has the necessary IAM roles.

    To find out what’s causing the issue, you can enable DEBUG logging level as mentioned above.

    PostgreSQL: java.net.SocketException: already connected issue

    We found this exception to be common if your Maven project’s parent is spring-boot version 1.5.x, or in any other circumstance that would cause the version of the org.postgresql:postgresql dependency to be an older one (e.g., 9.4.1212.jre7).

    To fix this, re-declare the dependency in its correct version. +For example, in Maven:

    <dependency>
    +  <groupId>org.postgresql</groupId>
    +  <artifactId>postgresql</artifactId>
    +  <version>42.1.1</version>
    +</dependency>
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__spring_resources.html b/Greenwich.SR5/multi/multi__spring_resources.html new file mode 100644 index 00000000..dec3edb5 --- /dev/null +++ b/Greenwich.SR5/multi/multi__spring_resources.html @@ -0,0 +1,18 @@ + + + 156. Spring Resources

    156. Spring Resources

    Spring Resources are an abstraction for a number of low-level resources, such as file system files, classpath files, servlet context-relative files, etc. +Spring Cloud GCP adds a new resource type: a Google Cloud Storage (GCS) object.

    A Spring Boot starter is provided to auto-configure the various Storage components.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-storage</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage'
    +}

    This starter is also available from Spring Initializr through the GCP Storage entry.

    156.1 Google Cloud Storage

    The Spring Resource Abstraction for Google Cloud Storage allows GCS objects to be accessed by their GCS URL using the @Value annotation:

    @Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]")
    +private Resource gcsResource;

    …​or the Spring application context

    SpringApplication.run(...).getResource("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]");

    This creates a Resource object that can be used to read the object, among other possible operations.

    It is also possible to write to a Resource, although a WriteableResource is required.

    @Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]")
    +private Resource gcsResource;
    +...
    +try (OutputStream os = ((WritableResource) gcsResource).getOutputStream()) {
    +  os.write("foo".getBytes());
    +}

    To work with the Resource as a Google Cloud Storage resource, cast it to GoogleStorageResource.

    If the resource path refers to an object on Google Cloud Storage (as opposed to a bucket), then the getBlob method can be called to obtain a Blob. +This type represents a GCS file, which has associated metadata, such as content-type, that can be set. +The createSignedUrl method can also be used to obtain signed URLs for GCS objects. +However, creating signed URLs requires that the resource was created using service account credentials.

    The Spring Boot Starter for Google Cloud Storage auto-configures the Storage bean required by the spring-cloud-gcp-storage module, based on the CredentialsProvider provided by the Spring Boot GCP starter.

    156.1.1 Setting the Content Type

    You can set the content-type of Google Cloud Storage files from their corresponding Resource objects:

    ((GoogleStorageResource)gcsResource).getBlob().toBuilder().setContentType("text/html").build().update();

    156.2 Configuration

    The Spring Boot Starter for Google Cloud Storage provides the following configuration options:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.storage.enabled

    Enables the GCP storage APIs.

    No

    true

    spring.cloud.gcp.storage.auto-create-files

    Creates files and buckets on Google Cloud Storage when writes are made to non-existent files

    No

    true

    spring.cloud.gcp.storage.credentials.location

    OAuth2 credentials for authenticating with the Google Cloud Storage API, if different from the ones in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.storage.credentials.encoded-key

    Base64-encoded contents of OAuth2 account private key for authenticating with the Google Cloud Storage API, if different from the ones in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.storage.credentials.scopes

    OAuth2 scope for Spring Cloud GCP Storage credentials

    No

    https://www.googleapis.com/auth/devstorage.read_write

    156.3 Sample

    A sample application and a codelab are available.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__stackdriver_logging.html b/Greenwich.SR5/multi/multi__stackdriver_logging.html new file mode 100644 index 00000000..6ce29cc9 --- /dev/null +++ b/Greenwich.SR5/multi/multi__stackdriver_logging.html @@ -0,0 +1,60 @@ + + + 161. Stackdriver Logging

    161. Stackdriver Logging

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-logging</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-logging'
    +}

    Stackdriver Logging is the managed logging service provided by Google Cloud Platform.

    This module provides support for associating a web request trace ID with the corresponding log entries. +It does so by retrieving the X-B3-TraceId value from the Mapped Diagnostic Context (MDC), which is set by Spring Cloud Sleuth. +If Spring Cloud Sleuth isn’t used, the configured TraceIdExtractor extracts the desired header value and sets it as the log entry’s trace ID. +This allows grouping of log messages by request, for example, in the Google Cloud Console Logs viewer.

    [Note]Note

    Due to the way logging is set up, the GCP project ID and credentials defined in application.properties are ignored. +Instead, you should set the GOOGLE_CLOUD_PROJECT and GOOGLE_APPLICATION_CREDENTIALS environment variables to the project ID and credentials private key location, respectively. +You can do this easily if you’re using the Google Cloud SDK, using the gcloud config set project [YOUR_PROJECT_ID] and gcloud auth application-default login commands, respectively.

    161.1 Web MVC Interceptor

    For use in Web MVC-based applications, TraceIdLoggingWebMvcInterceptor is provided that extracts the request trace ID from an HTTP request using a TraceIdExtractor and stores it in a thread-local, which can then be used in a logging appender to add the trace ID metadata to log messages.

    [Warning]Warning

    If Spring Cloud GCP Trace is enabled, the logging module disables itself and delegates log correlation to Spring Cloud Sleuth.

    LoggingWebMvcConfigurer configuration class is also provided to help register the TraceIdLoggingWebMvcInterceptor in Spring MVC applications.

    Applications hosted on the Google Cloud Platform include trace IDs under the x-cloud-trace-context header, which will be included in log entries. +However, if Sleuth is used the trace ID will be picked up from the MDC.

    161.2 Logback Support

    Currently, only Logback is supported and there are 2 possibilities to log to Stackdriver via this library with Logback: via direct API calls and through JSON-formatted console logs.

    161.2.1 Log via API

    A Stackdriver appender is available using org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml. +This appender builds a Stackdriver Logging log entry from a JUL or Logback log entry, adds a trace ID to it and sends it to Stackdriver Logging.

    STACKDRIVER_LOG_NAME and STACKDRIVER_LOG_FLUSH_LEVEL environment variables can be used to customize the STACKDRIVER appender.

    Your configuration may then look like this:

    <configuration>
    +  <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml" />
    +
    +  <root level="INFO">
    +    <appender-ref ref="STACKDRIVER" />
    +  </root>
    +</configuration>

    If you want to have more control over the log output, you can further configure the appender. +The following properties are available:

    PropertyDefault ValueDescription

    log

    spring.log

    The Stackdriver Log name. +This can also be set via the STACKDRIVER_LOG_NAME environmental variable.

    flushLevel

    WARN

    If a log entry with this level is encountered, trigger a flush of locally buffered log to Stackdriver Logging. +This can also be set via the STACKDRIVER_LOG_FLUSH_LEVEL environmental variable.

    161.2.2 Log via Console

    For Logback, a org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml file is made available for import to make it easier to configure the JSON Logback appender.

    Your configuration may then look something like this:

    <configuration>
    +  <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml" />
    +
    +  <root level="INFO">
    +    <appender-ref ref="CONSOLE_JSON" />
    +  </root>
    +</configuration>

    If your application is running on Google Kubernetes Engine, Google Compute Engine or Google App Engine Flexible, your console logging is automatically saved to Google Stackdriver Logging. +Therefore, you can just include org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml in your logging configuration, which logs JSON entries to the console. +The trace id will be set correctly.

    If you want to have more control over the log output, you can further configure the appender. +The following properties are available:

    PropertyDefault ValueDescription

    projectId

    If not set, default value is determined in the following order:

    +
    1. SPRING_CLOUD_GCP_LOGGING_PROJECT_ID Environmental Variable.
    2. Value of DefaultGcpProjectIdProvider.getProjectId()

    This is used to generate fully qualified Stackdriver Trace ID format: projects/[PROJECT-ID]/traces/[TRACE-ID].

    +

    This format is required to correlate trace between Stackdriver Trace and Stackdriver Logging.

    +

    If projectId is not set and cannot be determined, then it’ll log traceId without the fully qualified format.

    includeTraceId

    true

    Should the traceId be included

    includeSpanId

    true

    Should the spanId be included

    includeLevel

    true

    Should the severity be included

    includeThreadName

    true

    Should the thread name be included

    includeMDC

    true

    Should all MDC properties be included. +The MDC properties X-B3-TraceId, X-B3-SpanId and X-Span-Export provided by Spring Sleuth will get excluded as they get handled separately

    includeLoggerName

    true

    Should the name of the logger be included

    includeFormattedMessage

    true

    Should the formatted log message be included.

    includeExceptionInMessage

    true

    Should the stacktrace be appended to the formatted log message. +This setting is only evaluated if includeFormattedMessage is true

    includeContextName

    true

    Should the logging context be included

    includeMessage

    false

    Should the log message with blank placeholders be included

    includeException

    false

    Should the stacktrace be included as a own field

    This is an example of such an Logback configuration:

    <configuration >
    +  <property name="projectId" value="${projectId:-${GOOGLE_CLOUD_PROJECT}}"/>
    +
    +  <appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender">
    +    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
    +      <layout class="org.springframework.cloud.gcp.logging.StackdriverJsonLayout">
    +        <projectId>${projectId}</projectId>
    +
    +        <!--<includeTraceId>true</includeTraceId>-->
    +        <!--<includeSpanId>true</includeSpanId>-->
    +        <!--<includeLevel>true</includeLevel>-->
    +        <!--<includeThreadName>true</includeThreadName>-->
    +        <!--<includeMDC>true</includeMDC>-->
    +        <!--<includeLoggerName>true</includeLoggerName>-->
    +        <!--<includeFormattedMessage>true</includeFormattedMessage>-->
    +        <!--<includeExceptionInMessage>true</includeExceptionInMessage>-->
    +        <!--<includeContextName>true</includeContextName>-->
    +        <!--<includeMessage>false</includeMessage>-->
    +        <!--<includeException>false</includeException>-->
    +      </layout>
    +    </encoder>
    +  </appender>
    +</configuration>

    161.3 Sample

    A Sample Spring Boot Application is provided to show how to use the Cloud logging starter.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__standalone_streaming_applications.html b/Greenwich.SR5/multi/multi__standalone_streaming_applications.html new file mode 100644 index 00000000..0be1157f --- /dev/null +++ b/Greenwich.SR5/multi/multi__standalone_streaming_applications.html @@ -0,0 +1,4 @@ + + + 131. Standalone Streaming Applications

    131. Standalone Streaming Applications

    To send or receive messages from a broker (such as RabbitMQ or Kafka) you can leverage spring-cloud-stream project and it’s integration with Spring Cloud Function. +Please refer to Spring Cloud Function section of the Spring Cloud Stream reference manual for more details and examples.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__standalone_web_applications.html b/Greenwich.SR5/multi/multi__standalone_web_applications.html new file mode 100644 index 00000000..d7f1f07c --- /dev/null +++ b/Greenwich.SR5/multi/multi__standalone_web_applications.html @@ -0,0 +1,16 @@ + + + 130. Standalone Web Applications

    130. Standalone Web Applications

    The spring-cloud-function-web module has autoconfiguration that +activates when it is included in a Spring Boot web application (with +MVC support). There is also a spring-cloud-starter-function-web to +collect all the optional dependencies in case you just want a simple +getting started experience.

    With the web configurations activated your app will have an MVC +endpoint (on "/" by default, but configurable with +spring.cloud.function.web.path) that can be used to access the +functions in the application context. The supported content types are +plain text and JSON.

    MethodPathRequestResponseStatus

    GET

    /{supplier}

    -

    Items from the named supplier

    200 OK

    POST

    /{consumer}

    JSON object or text

    Mirrors input and pushes request body into consumer

    202 Accepted

    POST

    /{consumer}

    JSON array or text with new lines

    Mirrors input and pushes body into consumer one by one

    202 Accepted

    POST

    /{function}

    JSON object or text

    The result of applying the named function

    200 OK

    POST

    /{function}

    JSON array or text with new lines

    The result of applying the named function

    200 OK

    GET

    /{function}/{item}

    -

    Convert the item into an object and return the result of applying the function

    200 OK

    As the table above shows the behaviour of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or Flux), then the response will also contain a single value. +For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream".

    If there is only a single function (consumer etc.) in the catalog, the name in the path is optional. +Composite functions can be addressed using pipes or commas to separate function names (pipes are legal in URL paths, but a bit awkward to type on the command line).

    For cases where there is more then a single function in catalog and you want to map a specific function to the root +path (e.g., "/"), or you want to compose several functions and then map to the root path you can do so by providing +spring.cloud.function.definition property which essentially used by spring-=cloud-function-web module to provide +default mapping for cases where there is some type of a conflict (e.g., more then one function available etc).

    For example,

    --spring.cloud.function.definition=foo|bar

    The above property will compose 'foo' and 'bar' function and map the composed function to the "/" path.

    Functions and consumers that are declared with input and output in Message<?> will see the request headers on the input messages, and the output message headers will be converted to HTTP headers.

    When POSTing text the response format might be different with Spring Boot 2.0 and older versions, depending on the content negotiation (provide content type and accpt headers for the best results).

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__starters.html b/Greenwich.SR5/multi/multi__starters.html new file mode 100644 index 00000000..aedf45c4 --- /dev/null +++ b/Greenwich.SR5/multi/multi__starters.html @@ -0,0 +1,22 @@ + + + 137. Starters

    137. Starters

    Starters are convenient dependency descriptors you can include in your +application. Include a starter to get the dependencies and Spring Boot +auto-configuration for a feature set.

    StarterFeatures
    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes</artifactId>
    +</dependency>

    Discovery Client implementation that +resolves service names to Kubernetes Services.

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-config</artifactId>
    +</dependency>

    Load application properties from Kubernetes +ConfigMaps and Secrets. +Reload application properties when a ConfigMap or +Secret changes.

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
    +</dependency>

    Ribbon client-side load balancer with +server list obtained from Kubernetes Endpoints.

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-all</artifactId>
    +</dependency>

    All Spring Cloud Kubernetes features.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__testing.html b/Greenwich.SR5/multi/multi__testing.html new file mode 100644 index 00000000..6ec1b023 --- /dev/null +++ b/Greenwich.SR5/multi/multi__testing.html @@ -0,0 +1,56 @@ + + + 35. Testing

    35. Testing

    Spring Cloud Stream provides support for testing your microservice applications without connecting to a messaging system. +You can do that by using the TestSupportBinder provided by the spring-cloud-stream-test-support library, which can be added as a test dependency to the application, as shown in the following example:

       <dependency>
    +       <groupId>org.springframework.cloud</groupId>
    +       <artifactId>spring-cloud-stream-test-support</artifactId>
    +       <scope>test</scope>
    +   </dependency>
    [Note]Note

    The TestSupportBinder uses the Spring Boot autoconfiguration mechanism to supersede the other binders found on the classpath. +Therefore, when adding a binder as a dependency, you must make sure that the test scope is being used.

    The TestSupportBinder lets you interact with the bound channels and inspect any messages sent and received by the application.

    For outbound message channels, the TestSupportBinder registers a single subscriber and retains the messages emitted by the application in a MessageCollector. +They can be retrieved during tests and have assertions made against them.

    You can also send messages to inbound message channels so that the consumer application can consume the messages. +The following example shows how to test both input and output channels on a processor:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
    +public class ExampleTest {
    +
    +  @Autowired
    +  private Processor processor;
    +
    +  @Autowired
    +  private MessageCollector messageCollector;
    +
    +  @Test
    +  @SuppressWarnings("unchecked")
    +  public void testWiring() {
    +    Message<String> message = new GenericMessage<>("hello");
    +    processor.input().send(message);
    +    Message<String> received = (Message<String>) messageCollector.forChannel(processor.output()).poll();
    +    assertThat(received.getPayload(), equalTo("hello world"));
    +  }
    +
    +
    +  @SpringBootApplication
    +  @EnableBinding(Processor.class)
    +  public static class MyProcessor {
    +
    +    @Autowired
    +    private Processor channels;
    +
    +    @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
    +    public String transform(String in) {
    +      return in + " world";
    +    }
    +  }
    +}

    In the preceding example, we create an application that has an input channel and an output channel, both bound through the Processor interface. +The bound interface is injected into the test so that we can have access to both channels. +We send a message on the input channel, and we use the MessageCollector provided by Spring Cloud Stream’s test support to capture that the message has been sent to the output channel as a result. +Once we have received the message, we can validate that the component functions correctly.

    35.1 Disabling the Test Binder Autoconfiguration

    The intent behind the test binder superseding all the other binders on the classpath is to make it easy to test your applications without making changes to your production dependencies. +In some cases (for example, integration tests) it is useful to use the actual production binders instead, and that requires disabling the test binder autoconfiguration. +To do so, you can exclude the org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration class by using one of the Spring Boot autoconfiguration exclusion mechanisms, as shown in the following example:

        @SpringBootApplication(exclude = TestSupportBinderAutoConfiguration.class)
    +    @EnableBinding(Processor.class)
    +    public static class MyProcessor {
    +
    +        @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
    +        public String transform(String in) {
    +            return in + " world";
    +        }
    +    }

    When autoconfiguration is disabled, the test binder is available on the classpath, and its defaultCandidate property is set to false so that it does not interfere with the regular user configuration. It can be referenced under the name, test, as shown in the following example:

    spring.cloud.stream.defaultBinder=test

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__tls_ssl.html b/Greenwich.SR5/multi/multi__tls_ssl.html new file mode 100644 index 00000000..25ac69c6 --- /dev/null +++ b/Greenwich.SR5/multi/multi__tls_ssl.html @@ -0,0 +1,36 @@ + + + 118. TLS / SSL

    118. TLS / SSL

    The Gateway can listen for requests on https by following the usual Spring server configuration. Example:

    application.yml.  +

    server:
    +  ssl:
    +    enabled: true
    +    key-alias: scg
    +    key-store-password: scg1234
    +    key-store: classpath:scg-keystore.p12
    +    key-store-type: PKCS12

    +

    Gateway routes can be routed to both http and https backends. If routing to a https backend then the Gateway can be configured to trust all downstream certificates with the following configuration:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      httpclient:
    +        ssl:
    +          useInsecureTrustManager: true

    +

    Using an insecure trust manager is not suitable for production. For a production deployment the Gateway can be configured with a set of known certificates that it can trust with the follwing configuration:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      httpclient:
    +        ssl:
    +          trustedX509Certificates:
    +          - cert1.pem
    +          - cert2.pem

    +

    If the Spring Cloud Gateway is not provisioned with trusted certificates the default trust store is used (which can be overriden with system property javax.net.ssl.trustStore).

    118.1 TLS Handshake

    The Gateway maintains a client pool that it uses to route to backends. When communicating over https the client initiates a TLS handshake. A number of timeouts are assoicated with this handshake. These timeouts can be configured (defaults shown):

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      httpclient:
    +        ssl:
    +          handshake-timeout-millis: 10000
    +          close-notify-flush-timeout-millis: 3000
    +          close-notify-read-timeout-millis: 0

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__tracing_bus_events.html b/Greenwich.SR5/multi/multi__tracing_bus_events.html new file mode 100644 index 00000000..e053114d --- /dev/null +++ b/Greenwich.SR5/multi/multi__tracing_bus_events.html @@ -0,0 +1,42 @@ + + + 48. Tracing Bus Events

    48. Tracing Bus Events

    Bus events (subclasses of RemoteApplicationEvent) can be traced by setting +spring.cloud.bus.trace.enabled=true. If you do so, the Spring Boot TraceRepository +(if it is present) shows each event sent and all the acks from each service instance. The +following example comes from the /trace endpoint:

    {
    +  "timestamp": "2015-11-26T10:24:44.411+0000",
    +  "info": {
    +    "signal": "spring.cloud.bus.ack",
    +    "type": "RefreshRemoteApplicationEvent",
    +    "id": "c4d374b7-58ea-4928-a312-31984def293b",
    +    "origin": "stores:8081",
    +    "destination": "*:**"
    +  }
    +  },
    +  {
    +  "timestamp": "2015-11-26T10:24:41.864+0000",
    +  "info": {
    +    "signal": "spring.cloud.bus.sent",
    +    "type": "RefreshRemoteApplicationEvent",
    +    "id": "c4d374b7-58ea-4928-a312-31984def293b",
    +    "origin": "customers:9000",
    +    "destination": "*:**"
    +  }
    +  },
    +  {
    +  "timestamp": "2015-11-26T10:24:41.862+0000",
    +  "info": {
    +    "signal": "spring.cloud.bus.ack",
    +    "type": "RefreshRemoteApplicationEvent",
    +    "id": "c4d374b7-58ea-4928-a312-31984def293b",
    +    "origin": "customers:9000",
    +    "destination": "*:**"
    +  }
    +}

    The preceding trace shows that a RefreshRemoteApplicationEvent was sent from +customers:9000, broadcast to all services, and received (acked) by customers:9000 and +stores:8081.

    To handle the ack signals yourself, you could add an @EventListener for the +AckRemoteApplicationEvent and SentApplicationEvent types to your app (and enable +tracing). Alternatively, you could tap into the TraceRepository and mine the data from +there.

    [Note]Note

    Any Bus application can trace acks. However, sometimes, it is +useful to do this in a central service that can do more complex +queries on the data or forward it to a specialized tracing service.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__using_the_pluggable_architecture.html b/Greenwich.SR5/multi/multi__using_the_pluggable_architecture.html new file mode 100644 index 00000000..dd22d5c8 --- /dev/null +++ b/Greenwich.SR5/multi/multi__using_the_pluggable_architecture.html @@ -0,0 +1,471 @@ + + + 96. Using the Pluggable Architecture

    96. Using the Pluggable Architecture

    You may encounter cases where you have your contracts have been defined in other formats, +such as YAML, RAML or PACT. In those cases, you still want to benefit from the automatic +generation of tests and stubs. You can add your own implementation for generating both +tests and stubs. Also, you can customize the way tests are generated (for example, you +can generate tests for other languages) and the way stubs are generated (for example, you +can generate stubs for other HTTP server implementations).

    96.1 Custom Contract Converter

    The ContractConverter interface lets you register your own implementation of a contract +structure converter. The following code listing shows the ContractConverter interface:

    package org.springframework.cloud.contract.spec
    +
    +/**
    + * Converter to be used to convert FROM {@link File} TO {@link Contract}
    + * and from {@link Contract} to {@code T}
    + *
    + * @param <T >     - type to which we want to convert the contract
    + *
    + * @author Marcin Grzejszczak
    + * @since 1.1.0
    + */
    +interface ContractConverter<T> extends ContractStorer<T> {
    +
    +	/**
    +	 * Should this file be accepted by the converter. Can use the file extension
    +	 * to check if the conversion is possible.
    +	 *
    +	 * @param file - file to be considered for conversion
    +	 * @return - {@code true} if the given implementation can convert the file
    +	 */
    +	boolean isAccepted(File file)
    +
    +	/**
    +	 * Converts the given {@link File} to its {@link Contract} representation
    +	 *
    +	 * @param file - file to convert
    +	 * @return - {@link Contract} representation of the file
    +	 */
    +	Collection<Contract> convertFrom(File file)
    +
    +	/**
    +	 * Converts the given {@link Contract} to a {@link T} representation
    +	 *
    +	 * @param contract - the parsed contract
    +	 * @return - {@link T} the type to which we do the conversion
    +	 */
    +	T convertTo(Collection<Contract> contract)
    +}

    Your implementation must define the condition on which it should start the +conversion. Also, you must define how to perform that conversion in both directions.

    [Important]Important

    Once you create your implementation, you must create a +/META-INF/spring.factories file in which you provide the fully qualified name of your +implementation.

    The following example shows a typical spring.factories file:

    org.springframework.cloud.contract.spec.ContractConverter=\
    +org.springframework.cloud.contract.verifier.converter.YamlContractConverter

    96.1.1 Pact Converter

    Spring Cloud Contract includes support for Pact representation of +contracts up until v4. Instead of using the Groovy DSL, you can use Pact files. In this section, we +present how to add Pact support for your project. Note however that not all functionality is supported. +Starting with v3 you can combine multiple matcher for the same element; +you can use matchers for the body, headers, request and path; and you can use value generators. +Spring Cloud Contract currently only supports multiple matchers that are combined using the AND rule logic. +Next to that the request and path matchers are skipped during the conversion. +When using a date, time or datetime value generator with a given format, +the given format will be skipped and the ISO format will be used.

    In order to properly support the Spring Cloud Contract way of doing messaging +with Pact you’ll have to provide some additional meta data entries. Below you can find a list of such entries:

    • to define the destination to which a message gets sent, you have to +set a metaData entry in the Pact file, with key sentTo equal to the destination to which a message is to be sent. E.g. "metaData": { "sentTo": "activemq:output" }

    96.1.2 Pact Contract

    Consider following example of a Pact contract, which is a file under the +src/test/resources/contracts folder.

    {
    +  "provider": {
    +    "name": "Provider"
    +  },
    +  "consumer": {
    +    "name": "Consumer"
    +  },
    +  "interactions": [
    +    {
    +      "description": "",
    +      "request": {
    +        "method": "PUT",
    +        "path": "/fraudcheck",
    +        "headers": {
    +          "Content-Type": "application/vnd.fraud.v1+json"
    +        },
    +        "body": {
    +          "clientId": "1234567890",
    +          "loanAmount": 99999
    +        },
    +        "generators": {
    +          "body": {
    +            "$.clientId": {
    +              "type": "Regex",
    +              "regex": "[0-9]{10}"
    +            }
    +          }
    +        },
    +        "matchingRules": {
    +          "header": {
    +            "Content-Type": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "application/vnd\\.fraud\\.v1\\+json.*"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          },
    +          "body": {
    +            "$.clientId": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "[0-9]{10}"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          }
    +        }
    +      },
    +      "response": {
    +        "status": 200,
    +        "headers": {
    +          "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
    +        },
    +        "body": {
    +          "fraudCheckStatus": "FRAUD",
    +          "rejectionReason": "Amount too high"
    +        },
    +        "matchingRules": {
    +          "header": {
    +            "Content-Type": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "application/vnd\\.fraud\\.v1\\+json.*"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          },
    +          "body": {
    +            "$.fraudCheckStatus": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "FRAUD"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          }
    +        }
    +      }
    +    }
    +  ],
    +  "metadata": {
    +    "pact-specification": {
    +      "version": "3.0.0"
    +    },
    +    "pact-jvm": {
    +      "version": "3.5.13"
    +    }
    +  }
    +}

    The remainder of this section about using Pact refers to the preceding file.

    96.1.3 Pact for Producers

    On the producer side, you must add two additional dependencies to your plugin +configuration. One is the Spring Cloud Contract Pact support, and the other represents +the current Pact version that you use.

    Maven.  +

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +	</configuration>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-contract-pact</artifactId>
    +			<version>${spring-cloud-contract.version}</version>
    +		</dependency>
    +	</dependencies>
    +</plugin>

    +

    Gradle.  +

    classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}"

    +

    When you execute the build of your application, a test will be generated. The generated +test might be as follows:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +	// given:
    +		MockMvcRequestSpecification request = given()
    +				.header("Content-Type", "application/vnd.fraud.v1+json")
    +				.body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");
    +
    +	// when:
    +		ResponseOptions response = given().spec(request)
    +				.put("/fraudcheck");
    +
    +	// then:
    +		assertThat(response.statusCode()).isEqualTo(200);
    +		assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
    +	// and:
    +		DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +		assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
    +	// and:
    +		assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
    +}

    The corresponding generated stub might be as follows:

    {
    +  "id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
    +  "uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
    +  "request" : {
    +    "url" : "/fraudcheck",
    +    "method" : "PUT",
    +    "headers" : {
    +      "Content-Type" : {
    +        "matches" : "application/vnd\\.fraud\\.v1\\+json.*"
    +      }
    +    },
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
    +    } ]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
    +    "headers" : {
    +      "Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
    +    },
    +    "transformers" : [ "response-template" ]
    +  },
    +}

    96.1.4 Pact for Consumers

    On the consumer side, you must add two additional dependencies to your project +dependencies. One is the Spring Cloud Contract Pact support, and the other represents the +current Pact version that you use.

    Maven.  +

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-pact</artifactId>
    +	<scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile "org.springframework.cloud:spring-cloud-contract-pact"

    +

    96.2 Using the Custom Test Generator

    If you want to generate tests for languages other than Java or you are not happy with the +way the verifier builds Java tests, you can register your own implementation.

    The SingleTestGenerator interface lets you register your own implementation. The +following code listing shows the SingleTestGenerator interface:

    package org.springframework.cloud.contract.verifier.builder
    +
    +
    +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
    +import org.springframework.cloud.contract.verifier.file.ContractMetadata
    +
    +/**
    + * Builds a single test.
    + *
    + * @since 1.1.0
    + */
    +trait SingleTestGenerator {
    +
    +	/**
    +	 * Creates contents of a single test class in which all test scenarios from
    +	 * the contract metadata should be placed.
    +	 *
    +	 * @param properties - properties passed to the plugin
    +	 * @param listOfFiles - list of parsed contracts with additional metadata
    +	 * @param className - the name of the generated test class
    +	 * @param classPackage - the name of the package in which the test class should be stored
    +	 * @param includedDirectoryRelativePath - relative path to the included directory
    +	 * @return contents of a single test class
    +	 * @deprecated use{@link SingleTestGenerator#buildClass(ContractVerifierConfigProperties, Collection, String, GeneratedClassData)}
    +	 */
    +	@Deprecated
    +	abstract String buildClass(ContractVerifierConfigProperties properties,
    +			Collection<ContractMetadata> listOfFiles, String className, String classPackage, String includedDirectoryRelativePath)
    +
    +	/**
    +	 * Creates contents of a single test class in which all test scenarios from
    +	 * the contract metadata should be placed.
    +	 *
    +	 * @param properties - properties passed to the plugin
    +	 * @param listOfFiles - list of parsed contracts with additional metadata
    +	 * @param generatedClassData - information about the generated class
    +	 * @param includedDirectoryRelativePath - relative path to the included directory
    +	 * @return contents of a single test class
    +	 */
    +	String buildClass(ContractVerifierConfigProperties properties,
    +			Collection<ContractMetadata> listOfFiles, String includedDirectoryRelativePath, GeneratedClassData generatedClassData) {
    +		String className = generatedClassData.className
    +		String classPackage = generatedClassData.classPackage
    +		String path = includedDirectoryRelativePath
    +		return buildClass(properties, listOfFiles, className, classPackage, path)
    +	}
    +
    +	/**
    +	 * Extension that should be appended to the generated test class. E.g. {@code .java} or {@code .php}
    +	 *
    +	 * @param properties - properties passed to the plugin
    +	 */
    +	abstract String fileExtension(ContractVerifierConfigProperties properties)
    +
    +	static class GeneratedClassData {
    +		public final String className
    +		public final String classPackage
    +		public final java.nio.file.Path testClassPath
    +
    +		GeneratedClassData(String className, String classPackage,
    +				java.nio.file.Path testClassPath) {
    +			this.className = className
    +			this.classPackage = classPackage
    +			this.testClassPath = testClassPath
    +		}
    +	}
    +}

    Again, you must provide a spring.factories file, such as the one shown in the following +example:

    org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
    +com.example.MyGenerator

    96.3 Using the Custom Stub Generator

    If you want to generate stubs for stub servers other than WireMock, you can plug in your +own implementation of the StubGenerator interface. The following code listing shows the +StubGenerator interface:

    package org.springframework.cloud.contract.verifier.converter
    +
    +import groovy.transform.CompileStatic
    +
    +import org.springframework.cloud.contract.spec.Contract
    +import org.springframework.cloud.contract.verifier.file.ContractMetadata
    +
    +/**
    + * Converts contracts into their stub representation.
    + *
    + * @since 1.1.0
    + */
    +@CompileStatic
    +interface StubGenerator {
    +
    +	/**
    +	 * @return {@code true} if the converter can handle the file to convert it into a stub.
    +	 */
    +	boolean canHandleFileName(String fileName)
    +
    +	/**
    +	 * @return the collection of converted contracts into stubs. One contract can
    +	 * result in multiple stubs.
    +	 */
    +	Map<Contract, String> convertContents(String rootName, ContractMetadata content)
    +
    +	/**
    +	 * @return the name of the converted stub file. If you have multiple contracts
    +	 * in a single file then a prefix will be added to the generated file. If you
    +	 * provide the {@link Contract#name} field then that field will override the
    +	 * generated file name.
    +	 *
    +	 * Example: name of file with 2 contracts is {@code foo.groovy}, it will be
    +	 * converted by the implementation to {@code foo.json}. The recursive file
    +	 * converter will create two files {@code 0_foo.json} and {@code 1_foo.json}
    +	 */
    +	String generateOutputFileNameForInput(String inputFileName)
    +}

    Again, you must provide a spring.factories file, such as the one shown in the following +example:

    # Stub converters
    +org.springframework.cloud.contract.verifier.converter.StubGenerator=\
    +org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter

    The default implementation is the WireMock stub generation.

    [Tip]Tip

    You can provide multiple stub generator implementations. For example, from a single +DSL, you can produce both WireMock stubs and Pact files.

    96.4 Using the Custom Stub Runner

    If you decide to use a custom stub generation, you also need a custom way of running +stubs with your different stub provider.

    Assume that you use Moco to build your stubs and that +you have written a stub generator and placed your stubs in a JAR file.

    In order for Stub Runner to know how to run your stubs, you have to define a custom +HTTP Stub server implementation, which might resemble the following example:

    package org.springframework.cloud.contract.stubrunner.provider.moco
    +
    +import com.github.dreamhead.moco.bootstrap.arg.HttpArgs
    +import com.github.dreamhead.moco.runner.JsonRunner
    +import com.github.dreamhead.moco.runner.RunnerSetting
    +import groovy.util.logging.Commons
    +
    +import org.springframework.cloud.contract.stubrunner.HttpServerStub
    +import org.springframework.util.SocketUtils
    +
    +@Commons
    +class MocoHttpServerStub implements HttpServerStub {
    +
    +	private boolean started
    +	private JsonRunner runner
    +	private int port
    +
    +	@Override
    +	int port() {
    +		if (!isRunning()) {
    +			return -1
    +		}
    +		return port
    +	}
    +
    +	@Override
    +	boolean isRunning() {
    +		return started
    +	}
    +
    +	@Override
    +	HttpServerStub start() {
    +		return start(SocketUtils.findAvailableTcpPort())
    +	}
    +
    +	@Override
    +	HttpServerStub start(int port) {
    +		this.port = port
    +		return this
    +	}
    +
    +	@Override
    +	HttpServerStub stop() {
    +		if (!isRunning()) {
    +			return this
    +		}
    +		this.runner.stop()
    +		return this
    +	}
    +
    +	@Override
    +	HttpServerStub registerMappings(Collection<File> stubFiles) {
    +		List<RunnerSetting> settings = stubFiles.findAll { it.name.endsWith("json") }
    +			.collect {
    +			log.info("Trying to parse [${it.name}]")
    +			try {
    +				return RunnerSetting.aRunnerSetting().withStream(it.newInputStream()).
    +					build()
    +			}
    +			catch (Exception e) {
    +				log.warn("Exception occurred while trying to parse file [${it.name}]", e)
    +				return null
    +			}
    +		}.findAll { it }
    +		this.runner = JsonRunner.newJsonRunnerWithSetting(settings,
    +			HttpArgs.httpArgs().withPort(this.port).build())
    +		this.runner.run()
    +		this.started = true
    +		return this
    +	}
    +
    +	@Override
    +	String registeredMappings() {
    +		return ""
    +	}
    +
    +	@Override
    +	boolean isAccepted(File file) {
    +		return file.name.endsWith(".json")
    +	}
    +}

    Then, you can register it in your spring.factories file, as shown in the following +example:

    org.springframework.cloud.contract.stubrunner.HttpServerStub=\
    +org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub

    Now you can run stubs with Moco.

    [Important]Important

    If you do not provide any implementation, then the default (WireMock) +implementation is used. If you provide more than one, the first one on the list is used.

    96.5 Using the Custom Stub Downloader

    You can customize the way your stubs are downloaded by creating an implementation of the +StubDownloaderBuilder interface, as shown in the following example:

    package com.example;
    +
    +class CustomStubDownloaderBuilder implements StubDownloaderBuilder {
    +
    +	@Override
    +	public StubDownloader build(final StubRunnerOptions stubRunnerOptions) {
    +		return new StubDownloader() {
    +			@Override
    +			public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
    +					StubConfiguration config) {
    +				File unpackedStubs = retrieveStubs();
    +				return new AbstractMap.SimpleEntry<>(
    +						new StubConfiguration(config.getGroupId(), config.getArtifactId(), version,
    +								config.getClassifier()), unpackedStubs);
    +			}
    +
    +			File retrieveStubs() {
    +			    // here goes your custom logic to provide a folder where all the stubs reside
    +			}
    +}

    Then you can register it in your spring.factories file, as shown in the following +example:

    # Example of a custom Stub Downloader Provider
    +org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
    +com.example.CustomStubDownloaderBuilder

    Now you can pick a folder with the source of your stubs.

    [Important]Important

    If you do not provide any implementation, then the default is used (scan classpath). +If you provide the stubsMode = StubRunnerProperties.StubsMode.LOCAL or +, stubsMode = StubRunnerProperties.StubsMode.REMOTE then the Aether implementation will be used +If you provide more than one, then the first one on the list is used.

    96.6 Using the SCM Stub Downloader

    Whenever the repositoryRoot starts with a SCM protocol +(currently we support only git://), the stub downloader will try +to clone the repository and use it as a source of contracts +to generate tests or stubs.

    Either via environment variables, system properties, properties set +inside the plugin or contracts repository configuration you can +tweak the downloader’s behaviour. Below you can find the list of +properties

    Table 96.1. SCM Stub Downloader properties

    Type of a property

    Name of the property

    Description

    * git.branch (plugin prop)

    * stubrunner.properties.git.branch (system prop)

    * STUBRUNNER_PROPERTIES_GIT_BRANCH (env prop)

    master

    Which branch to checkout

    * git.username (plugin prop)

    * stubrunner.properties.git.username (system prop)

    * STUBRUNNER_PROPERTIES_GIT_USERNAME (env prop)

     

    Git clone username

    * git.password (plugin prop)

    * stubrunner.properties.git.password (system prop)

    * STUBRUNNER_PROPERTIES_GIT_PASSWORD (env prop)

     

    Git clone password

    * git.no-of-attempts (plugin prop)

    * stubrunner.properties.git.no-of-attempts (system prop)

    * STUBRUNNER_PROPERTIES_GIT_NO_OF_ATTEMPTS (env prop)

    10

    Number of attempts to push the commits to origin

    * git.wait-between-attempts (Plugin prop)

    * stubrunner.properties.git.wait-between-attempts (system prop)

    * STUBRUNNER_PROPERTIES_GIT_WAIT_BETWEEN_ATTEMPTS (env prop)

    1000

    Number of millis to wait between attempts to push the commits to origin


    96.7 Using the Pact Stub Downloader

    Whenever the repositoryRoot starts with a Pact protocol +(starts with pact://), the stub downloader will try +to fetch the Pact contract definitions from the Pact Broker. +Whatever is set after pact:// will be parsed as the Pact Broker URL.

    Either via environment variables, system properties, properties set +inside the plugin or contracts repository configuration you can +tweak the downloader’s behaviour. Below you can find the list of +properties

    Table 96.2. SCM Stub Downloader properties

    Name of a property

    Default

    Description

    * pactbroker.host (plugin prop)

    * stubrunner.properties.pactbroker.host (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_HOST (env prop)

    Host from URL passed to repositoryRoot

    What is the URL of Pact Broker

    * pactbroker.port (plugin prop)

    * stubrunner.properties.pactbroker.port (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_PORT (env prop)

    Port from URL passed to repositoryRoot

    What is the port of Pact Broker

    * pactbroker.protocol (plugin prop)

    * stubrunner.properties.pactbroker.protocol (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_PROTOCOL (env prop)

    Protocol from URL passed to repositoryRoot

    What is the protocol of Pact Broker

    * pactbroker.tags (plugin prop)

    * stubrunner.properties.pactbroker.tags (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_TAGS (env prop)

    Version of the stub, or latest if version is +

    What tags should be used to fetch the stub

    * pactbroker.auth.scheme (plugin prop)

    * stubrunner.properties.pactbroker.auth.scheme (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_SCHEME (env prop)

    Basic

    What kind of authentication should be used to connect to the Pact Broker

    * pactbroker.auth.username (plugin prop)

    * stubrunner.properties.pactbroker.auth.username (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_USERNAME (env prop)

    The username passed to contractsRepositoryUsername (maven) or contractRepository.username (gradle)

    Username used to connect to the Pact Broker

    * pactbroker.auth.password (plugin prop)

    * stubrunner.properties.pactbroker.auth.password (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD (env prop)

    The password passed to contractsRepositoryPassword (maven) or contractRepository.password (gradle)

    Password used to connect to the Pact Broker

    * pactbroker.provider-name-with-group-id (plugin prop)

    * stubrunner.properties.pactbroker.provider-name-with-group-id (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_PROVIDER_NAME_WITH_GROUP_ID (env prop)

    false

    When true, the provider name will be a combination of groupId:artifactId. If false, just artifactId is used


    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__whats_new_in_2_0.html b/Greenwich.SR5/multi/multi__whats_new_in_2_0.html new file mode 100644 index 00000000..ccf03850 --- /dev/null +++ b/Greenwich.SR5/multi/multi__whats_new_in_2_0.html @@ -0,0 +1,33 @@ + + + 26. What’s New in 2.0?

    26. What’s New in 2.0?

    Spring Cloud Stream introduces a number of new features, enhancements, and changes. The following sections outline the most notable ones:

    26.1 New Features and Components

    • Polling Consumers: Introduction of polled consumers, which lets the application control message processing rates. +See Section 29.3.5, “Using Polled Consumers” for more details. +You can also read this blog post for more details.
    • Micrometer Support: Metrics has been switched to use Micrometer. +MeterRegistry is also provided as a bean so that custom applications can autowire it to capture custom metrics. +See Chapter 37, Metrics Emitter for more details.
    • New Actuator Binding Controls: New actuator binding controls let you both visualize and control the Bindings lifecycle. +For more details, see Section 30.6, “Binding visualization and control”.
    • Configurable RetryTemplate: Aside from providing properties to configure RetryTemplate, we now let you provide your own template, effectively overriding the one provided by the framework. +To use it, configure it as a @Bean in your application.

    26.2 Notable Enhancements

    This version includes the following notable enhancements:

    26.2.1 Both Actuator and Web Dependencies Are Now Optional

    This change slims down the footprint of the deployed application in the event neither actuator nor web dependencies required. +It also lets you switch between the reactive and conventional web paradigms by manually adding one of the following dependencies.

    The following listing shows how to add the conventional web framework:

    <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-web</artifactId>
    +</dependency>

    The following listing shows how to add the reactive web framework:

    <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-webflux</artifactId>
    +</dependency>

    The following list shows how to add the actuator dependency:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-actuator</artifactId>
    +</dependency>

    26.2.2 Content-type Negotiation Improvements

    One of the core themes for verion 2.0 is improvements (in both consistency and performance) around content-type negotiation and message conversion. +The following summary outlines the notable changes and improvements in this area. +See the Chapter 32, Content Type Negotiation section for more details. +Also this blog post contains more detail.

    • All message conversion is now handled only by MessageConverter objects.
    • We introduced the @StreamMessageConverter annotation to provide custom MessageConverter objects.
    • We introduced the default Content Type as application/json, which needs to be taken into consideration when migrating 1.3 application or operating in the mixed mode (that is, 1.3 producer → 2.0 consumer).
    • Messages with textual payloads and a contentType of text/…​ or …​/json are no longer converted to Message<String> for cases where the argument type of the provided MessageHandler can not be determined (that is, public void handle(Message<?> message) or public void handle(Object payload)). +Furthermore, a strong argument type may not be enough to properly convert messages, so the contentType header may be used as a supplement by some MessageConverters.

    26.3 Notable Deprecations

    As of version 2.0, the following items have been deprecated:

    26.3.1 Java Serialization (Java Native and Kryo)

    JavaSerializationMessageConverter and KryoMessageConverter remain for now. However, we plan to move them out of the core packages and support in the future. +The main reason for this deprecation is to flag the issue that type-based, language-specific serialization could cause in distributed environments, where Producers and Consumers may depend on different JVM versions or have different versions of supporting libraries (that is, Kryo). +We also wanted to draw the attention to the fact that Consumers and Producers may not even be Java-based, so polyglot style serialization (i.e., JSON) is better suited.

    26.3.2 Deprecated Classes and Methods

    The following is a quick summary of notable deprecations. See the corresponding {spring-cloud-stream-javadoc-current}[javadoc] for more details.

    • SharedChannelRegistry. Use SharedBindingTargetRegistry.
    • Bindings. +Beans qualified by it are already uniquely identified by their type — for example, provided Source, Processor, or custom bindings:
    public interface Sample {
    +	String OUTPUT = "sampleOutput";
    +
    +	@Output(Sample.OUTPUT)
    +	MessageChannel output();
    +}
    • HeaderMode.raw. Use none, headers or embeddedHeaders
    • ProducerProperties.partitionKeyExtractorClass in favor of partitionKeyExtractorName and ProducerProperties.partitionSelectorClass in favor of partitionSelectorName. +This change ensures that both components are Spring configured and managed and are referenced in a Spring-friendly way.
    • BinderAwareRouterBeanPostProcessor. While the component remains, it is no longer a BeanPostProcessor and will be renamed in the future.
    • BinderProperties.setEnvironment(Properties environment). Use BinderProperties.setEnvironment(Map<String, Object> environment).

    This section goes into more detail about how you can work with Spring Cloud Stream. +It covers topics such as creating and running stream applications.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__why_do_you_need_spring_cloud_kubernetes.html b/Greenwich.SR5/multi/multi__why_do_you_need_spring_cloud_kubernetes.html new file mode 100644 index 00000000..d01671d1 --- /dev/null +++ b/Greenwich.SR5/multi/multi__why_do_you_need_spring_cloud_kubernetes.html @@ -0,0 +1,4 @@ + + + 136. Why do you need Spring Cloud Kubernetes?

    136. Why do you need Spring Cloud Kubernetes?

    Spring Cloud Kubernetes provide Spring Cloud common interface implementations that consume Kubernetes native services. +The main objective of the projects provided in this repository is to facilitate the integration of Spring Cloud and Spring Boot applications running inside Kubernetes.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi__zipkin_stream_span_consumer.html b/Greenwich.SR5/multi/multi__zipkin_stream_span_consumer.html new file mode 100644 index 00000000..18c96592 --- /dev/null +++ b/Greenwich.SR5/multi/multi__zipkin_stream_span_consumer.html @@ -0,0 +1,5 @@ + + + 63. Zipkin Stream Span Consumer

    63. Zipkin Stream Span Consumer

    [Important]Important

    We recommend using Zipkin’s native support for message-based span sending. +Starting from the Edgware release, the Zipkin Stream server is deprecated. +In the Finchley release, it got removed.

    If for some reason you need to create the deprecated Stream Zipkin server, see the Dalston Documentation.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_content-type-management.html b/Greenwich.SR5/multi/multi_content-type-management.html new file mode 100644 index 00000000..1c3b20a1 --- /dev/null +++ b/Greenwich.SR5/multi/multi_content-type-management.html @@ -0,0 +1,78 @@ + + + 32. Content Type Negotiation

    32. Content Type Negotiation

    Data transformation is one of the core features of any message-driven microservice architecture. Given that, in Spring Cloud Stream, such data +is represented as a Spring Message, a message may have to be transformed to a desired shape or size before reaching its destination. This is required for two reasons:

    1. To convert the contents of the incoming message to match the signature of the application-provided handler.
    2. To convert the contents of the outgoing message to the wire format.

    The wire format is typically byte[] (that is true for the Kafka and Rabbit binders), but it is governed by the binder implementation.

    In Spring Cloud Stream, message transformation is accomplished with an org.springframework.messaging.converter.MessageConverter.

    [Note]Note

    As a supplement to the details to follow, you may also want to read the following blog post.

    32.1 Mechanics

    To better understand the mechanics and the necessity behind content-type negotiation, we take a look at a very simple use case by using the following message handler as an example:

    @StreamListener(Processor.INPUT)
    +@SendTo(Processor.OUTPUT)
    +public String handle(Person person) {..}
    [Note]Note

    For simplicity, we assume that this is the only handler in the application (we assume there is no internal pipeline).

    The handler shown in the preceding example expects a Person object as an argument and produces a String type as an output. +In order for the framework to succeed in passing the incoming Message as an argument to this handler, it has to somehow transform the payload of the Message type from the wire format to a Person type. +In other words, the framework must locate and apply the appropriate MessageConverter. +To accomplish that, the framework needs some instructions from the user. +One of these instructions is already provided by the signature of the handler method itself (Person type). +Consequently, in theory, that should be (and, in some cases, is) enough. +However, for the majority of use cases, in order to select the appropriate MessageConverter, the framework needs an additional piece of information. +That missing piece is contentType.

    Spring Cloud Stream provides three mechanisms to define contentType (in order of precedence):

    1. HEADER: The contentType can be communicated through the Message itself. By providing a contentType header, you declare the content type to use to locate and apply the appropriate MessageConverter.
    2. BINDING: The contentType can be set per destination binding by setting the spring.cloud.stream.bindings.input.content-type property.

      [Note]Note

      The input segment in the property name corresponds to the actual name of the destination (which is “input” in our case). This approach lets you declare, on a per-binding basis, the content type to use to locate and apply the appropriate MessageConverter.

    3. DEFAULT: If contentType is not present in the Message header or the binding, the default application/json content type is used to +locate and apply the appropriate MessageConverter.

    As mentioned earlier, the preceding list also demonstrates the order of precedence in case of a tie. For example, a header-provided content type takes precedence over any other content type. +The same applies for a content type set on a per-binding basis, which essentially lets you override the default content type. +However, it also provides a sensible default (which was determined from community feedback).

    Another reason for making application/json the default stems from the interoperability requirements driven by distributed microservices architectures, where producer and consumer not only run in different JVMs but can also run on different non-JVM platforms.

    When the non-void handler method returns, if the the return value is already a Message, that Message becomes the payload. However, when the return value is not a Message, the new Message is constructed with the return value as the payload while inheriting +headers from the input Message minus the headers defined or filtered by SpringIntegrationProperties.messageHandlerNotPropagatedHeaders. +By default, there is only one header set there: contentType. This means that the new Message does not have contentType header set, thus ensuring that the contentType can evolve. +You can always opt out of returning a Message from the handler method where you can inject any header you wish.

    If there is an internal pipeline, the Message is sent to the next handler by going through the same process of conversion. However, if there is no internal pipeline or you have reached the end of it, the Message is sent back to the output destination.

    32.1.1 Content Type versus Argument Type

    As mentioned earlier, for the framework to select the appropriate MessageConverter, it requires argument type and, optionally, content type information. +The logic for selecting the appropriate MessageConverter resides with the argument resolvers (HandlerMethodArgumentResolvers), which trigger right before the invocation of the user-defined handler method (which is when the actual argument type is known to the framework). +If the argument type does not match the type of the current payload, the framework delegates to the stack of the +pre-configured MessageConverters to see if any one of them can convert the payload. +As you can see, the Object fromMessage(Message<?> message, Class<?> targetClass); +operation of the MessageConverter takes targetClass as one of its arguments. +The framework also ensures that the provided Message always contains a contentType header. +When no contentType header was already present, it injects either the per-binding contentType header or the default contentType header. +The combination of contentType argument type is the mechanism by which framework determines if message can be converted to a target type. +If no appropriate MessageConverter is found, an exception is thrown, which you can handle by adding a custom MessageConverter (see Section 32.3, “User-defined Message Converters”).

    But what if the payload type matches the target type declared by the handler method? In this case, there is nothing to convert, and the +payload is passed unmodified. While this sounds pretty straightforward and logical, keep in mind handler methods that take a Message<?> or Object as an argument. +By declaring the target type to be Object (which is an instanceof everything in Java), you essentially forfeit the conversion process.

    [Note]Note

    Do not expect Message to be converted into some other type based only on the contentType. +Remember that the contentType is complementary to the target type. +If you wish, you can provide a hint, which MessageConverter may or may not take into consideration.

    32.1.2 Message Converters

    MessageConverters define two methods:

    Object fromMessage(Message<?> message, Class<?> targetClass);
    +
    +Message<?> toMessage(Object payload, @Nullable MessageHeaders headers);

    It is important to understand the contract of these methods and their usage, specifically in the context of Spring Cloud Stream.

    The fromMessage method converts an incoming Message to an argument type. +The payload of the Message could be any type, and it is +up to the actual implementation of the MessageConverter to support multiple types. +For example, some JSON converter may support the payload type as byte[], String, and others. +This is important when the application contains an internal pipeline (that is, input → handler1 → handler2 →. . . → output) and the output of the upstream handler results in a Message which may not be in the initial wire format.

    However, the toMessage method has a more strict contract and must always convert Message to the wire format: byte[].

    So, for all intents and purposes (and especially when implementing your own converter) you regard the two methods as having the following signatures:

    Object fromMessage(Message<?> message, Class<?> targetClass);
    +
    +Message<byte[]> toMessage(Object payload, @Nullable MessageHeaders headers);

    32.2 Provided MessageConverters

    As mentioned earlier, the framework already provides a stack of MessageConverters to handle most common use cases. +The following list describes the provided MessageConverters, in order of precedence (the first MessageConverter that works is used):

    1. ApplicationJsonMessageMarshallingConverter: Variation of the org.springframework.messaging.converter.MappingJackson2MessageConverter. Supports conversion of the payload of the Message to/from POJO for cases when contentType is application/json (DEFAULT).
    2. TupleJsonMessageConverter: DEPRECATED Supports conversion of the payload of the Message to/from org.springframework.tuple.Tuple.
    3. ByteArrayMessageConverter: Supports conversion of the payload of the Message from byte[] to byte[] for cases when contentType is application/octet-stream. It is essentially a pass through and exists primarily for backward compatibility.
    4. ObjectStringMessageConverter: Supports conversion of any type to a String when contentType is text/plain. +It invokes Object’s toString() method or, if the payload is byte[], a new String(byte[]).
    5. JavaSerializationMessageConverter: DEPRECATED Supports conversion based on java serialization when contentType is application/x-java-serialized-object.
    6. KryoMessageConverter: DEPRECATED Supports conversion based on Kryo serialization when contentType is application/x-java-object.
    7. JsonUnmarshallingConverter: Similar to the ApplicationJsonMessageMarshallingConverter. It supports conversion of any type when contentType is application/x-java-object. +It expects the actual type information to be embedded in the contentType as an attribute (for example, application/x-java-object;type=foo.bar.Cat).

    When no appropriate converter is found, the framework throws an exception. When that happens, you should check your code and configuration and ensure you did not miss anything (that is, ensure that you provided a contentType by using a binding or a header). +However, most likely, you found some uncommon case (such as a custom contentType perhaps) and the current stack of provided MessageConverters +does not know how to convert. If that is the case, you can add custom MessageConverter. See Section 32.3, “User-defined Message Converters”.

    32.3 User-defined Message Converters

    Spring Cloud Stream exposes a mechanism to define and register additional MessageConverters. +To use it, implement org.springframework.messaging.converter.MessageConverter, configure it as a @Bean, and annotate it with @StreamMessageConverter. +It is then apended to the existing stack of `MessageConverter`s.

    [Note]Note

    It is important to understand that custom MessageConverter implementations are added to the head of the existing stack. +Consequently, custom MessageConverter implementations take precedence over the existing ones, which lets you override as well as add to the existing converters.

    The following example shows how to create a message converter bean to support a new content type called application/bar:

    @EnableBinding(Sink.class)
    +@SpringBootApplication
    +public static class SinkApplication {
    +
    +    ...
    +
    +    @Bean
    +    @StreamMessageConverter
    +    public MessageConverter customMessageConverter() {
    +        return new MyCustomMessageConverter();
    +    }
    +}
    +
    +public class MyCustomMessageConverter extends AbstractMessageConverter {
    +
    +    public MyCustomMessageConverter() {
    +        super(new MimeType("application", "bar"));
    +    }
    +
    +    @Override
    +    protected boolean supports(Class<?> clazz) {
    +        return (Bar.class.equals(clazz));
    +    }
    +
    +    @Override
    +    protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
    +        Object payload = message.getPayload();
    +        return (payload instanceof Bar ? payload : new Bar((byte[]) payload));
    +    }
    +}

    Spring Cloud Stream also provides support for Avro-based converters and schema evolution. +See Chapter 33, Schema Evolution Support for details.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_contract-dsl.html b/Greenwich.SR5/multi/multi_contract-dsl.html new file mode 100644 index 00000000..8be0d959 --- /dev/null +++ b/Greenwich.SR5/multi/multi_contract-dsl.html @@ -0,0 +1,2208 @@ + + + 94. Contract DSL

    94. Contract DSL

    Spring Cloud Contract supports out of the box 2 types of DSL. One written in +Groovy and one written in YAML.

    If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy +before. Knowledge of the language is not really needed, as the Contract DSL uses only a +tiny subset of it (only literals, method calls and closures). Also, the DSL is statically +typed, to make it programmer-readable without any knowledge of the DSL itself.

    [Important]Important

    Remember that, inside the Groovy contract file, you have to provide the fully +qualified name to the Contract class and make static imports, such as +org.springframework.cloud.spec.Contract.make { …​ }. You can also provide an import to +the Contract class: import org.springframework.cloud.spec.Contract and then call +Contract.make { …​ }.

    [Tip]Tip

    Spring Cloud Contract supports defining multiple contracts in a single file.

    The following is a complete example of a Groovy contract definition:

    The following is a complete example of a YAML contract definition:

    description: Some description
    +name: some name
    +priority: 8
    +ignored: true
    +request:
    +  url: /foo
    +  queryParameters:
    +    a: b
    +    b: c
    +  method: PUT
    +  headers:
    +    foo: bar
    +    fooReq: baz
    +  body:
    +    foo: bar
    +  matchers:
    +    body:
    +      - path: $.foo
    +        type: by_regex
    +        value: bar
    +    headers:
    +      - key: foo
    +        regex: bar
    +response:
    +  status: 200
    +  headers:
    +    foo2: bar
    +    foo3: foo33
    +    fooRes: baz
    +  body:
    +    foo2: bar
    +    foo3: baz
    +    nullValue: null
    +  matchers:
    +    body:
    +      - path: $.foo2
    +        type: by_regex
    +        value: bar
    +      - path: $.foo3
    +        type: by_command
    +        value: executeMe($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +    headers:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        command: andMeToo($it)
    [Tip]Tip

    You can compile contracts to stubs mapping using standalone maven command: +mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert

    94.1 Limitations

    [Warning]Warning

    Spring Cloud Contract Verifier does not properly support XML. Please use JSON or +help us implement this feature.

    [Warning]Warning

    The support for verifying the size of JSON arrays is experimental. If you want +to turn it on, please set the value of the following system property to true: +spring.cloud.contract.verifier.assert.size. By default, this feature is set to false. +You can also provide the assertJsonSize property in the plugin configuration.

    [Warning]Warning

    Because JSON structure can have any form, it can be impossible to parse it +properly when using the Groovy DSL and the value(consumer(…​), producer(…​)) notation in GString. That +is why you should use the Groovy Map notation.

    94.2 Common Top-Level elements

    The following sections describe the most common top-level elements:

    94.2.1 Description

    You can add a description to your contract. The description is arbitrary text. The +following code shows an example:

    Groovy DSL.  +

    			org.springframework.cloud.contract.spec.Contract.make {
    +				description('''
    +given:
    +	An input
    +when:
    +	Sth happens
    +then:
    +	Output
    +''')
    +			}

    +

    YAML.  +

    description: Some description
    +name: some name
    +priority: 8
    +ignored: true
    +request:
    +  url: /foo
    +  queryParameters:
    +    a: b
    +    b: c
    +  method: PUT
    +  headers:
    +    foo: bar
    +    fooReq: baz
    +  body:
    +    foo: bar
    +  matchers:
    +    body:
    +      - path: $.foo
    +        type: by_regex
    +        value: bar
    +    headers:
    +      - key: foo
    +        regex: bar
    +response:
    +  status: 200
    +  headers:
    +    foo2: bar
    +    foo3: foo33
    +    fooRes: baz
    +  body:
    +    foo2: bar
    +    foo3: baz
    +    nullValue: null
    +  matchers:
    +    body:
    +      - path: $.foo2
    +        type: by_regex
    +        value: bar
    +      - path: $.foo3
    +        type: by_command
    +        value: executeMe($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +    headers:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        command: andMeToo($it)

    +

    94.2.2 Name

    You can provide a name for your contract. Assume that you provided the following name: +should register a user. If you do so, the name of the autogenerated test is +validate_should_register_a_user. Also, the name of the stub in a WireMock stub is +should_register_a_user.json.

    [Important]Important

    You must ensure that the name does not contain any characters that make the +generated test not compile. Also, remember that, if you provide the same name for +multiple contracts, your autogenerated tests fail to compile and your generated stubs +override each other.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	name("some_special_name")
    +}

    +

    YAML.  +

    name: some name

    +

    94.2.3 Ignoring Contracts

    If you want to ignore a contract, you can either set a value of ignored contracts in the +plugin configuration or set the ignored property on the contract itself:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	ignored()
    +}

    +

    YAML.  +

    ignored: true

    +

    94.2.4 Passing Values from Files

    Starting with version 1.2.0, you can pass values from files. Assume that you have the +following resources in our project.

    └── src
    +    └── test
    +        └── resources
    +            └── contracts
    +                ├── readFromFile.groovy
    +                ├── request.json
    +                └── response.json

    Further assume that your contract is as follows:

    Groovy DSL.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +	request {
    +		method('PUT')
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(file("request.json"))
    +		url("/1")
    +	}
    +	response {
    +		status OK()
    +		body(file("response.json"))
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +}

    +

    YAML.  +

    request:
    +  method: GET
    +  url: /foo
    +  bodyFromFile: request.json
    +response:
    +  status: 200
    +  bodyFromFile: response.json

    +

    Further assume that the JSON files is as follows:

    request.json

    {
    +  "status": "REQUEST"
    +}

    response.json

    {
    +  "status": "RESPONSE"
    +}

    When test or stub generation takes place, the contents of the file is passed to the body +of a request or a response. The name of the file needs to be a file with location +relative to the folder in which the contract lays.

    If you need to pass the contents of a file in a binary form +it’s enough for you to use the fileAsBytes method in Groovy DSL or bodyFromFileAsBytes field in YAML.

    Groovy DSL.  +

    import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +	request {
    +		url("/1")
    +		method(PUT())
    +		headers {
    +			contentType(applicationOctetStream())
    +		}
    +		body(fileAsBytes("request.pdf"))
    +	}
    +	response {
    +		status 200
    +		body(fileAsBytes("response.pdf"))
    +		headers {
    +			contentType(applicationOctetStream())
    +		}
    +	}
    +}

    +

    YAML.  +

    request:
    +  url: /1
    +  method: PUT
    +  headers:
    +    Content-Type: application/octet-stream
    +  bodyFromFileAsBytes: request.pdf
    +response:
    +  status: 200
    +  bodyFromFileAsBytes: response.pdf
    +  headers:
    +    Content-Type: application/octet-stream

    +

    [Important]Important

    You should use this approach whenever you want to work with binary payloads both for HTTP and messaging.

    94.2.5 HTTP Top-Level Elements

    The following methods can be called in the top-level closure of a contract definition. +request and response are mandatory. priority is optional.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	// Definition of HTTP request part of the contract
    +	// (this can be a valid request or invalid depending
    +	// on type of contract being specified).
    +	request {
    +		method GET()
    +		url "/foo"
    +		//...
    +	}
    +
    +	// Definition of HTTP response part of the contract
    +	// (a service implementing this contract should respond
    +	// with following response after receiving request
    +	// specified in "request" part above).
    +	response {
    +		status 200
    +		//...
    +	}
    +
    +	// Contract priority, which can be used for overriding
    +	// contracts (1 is highest). Priority is optional.
    +	priority 1
    +}

    +

    YAML.  +

    priority: 8
    +request:
    +...
    +response:
    +...

    +

    [Important]Important

    If you want to make your contract have a higher value of priority +you need to pass a lower number to the priority tag / method. E.g. priority with +value 5 has higher priority than priority with value 10.

    94.3 Request

    The HTTP protocol requires only method and url to be specified in a request. The +same information is mandatory in request definition of the Contract.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		// HTTP request method (GET/POST/PUT/DELETE).
    +		method 'GET'
    +
    +		// Path component of request URL is specified as follows.
    +		urlPath('/users')
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    method: PUT
    +url: /foo

    +

    It is possible to specify an absolute rather than relative url, but using urlPath is +the recommended way, as doing so makes the tests host-independent.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'GET'
    +
    +		// Specifying `url` and `urlPath` in one contract is illegal.
    +		url('http://localhost:8888/users')
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +  method: PUT
    +  urlPath: /foo

    +

    request may contain query parameters.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +
    +		urlPath('/users') {
    +
    +			// Each parameter is specified in form
    +			// `'paramName' : paramValue` where parameter value
    +			// may be a simple literal or one of matcher functions,
    +			// all of which are used in this example.
    +			queryParameters {
    +
    +				// If a simple literal is used as value
    +				// default matcher function is used (equalTo)
    +				parameter 'limit': 100
    +
    +				// `equalTo` function simply compares passed value
    +				// using identity operator (==).
    +				parameter 'filter': equalTo("email")
    +
    +				// `containing` function matches strings
    +				// that contains passed substring.
    +				parameter 'gender': value(consumer(containing("[mf]")), producer('mf'))
    +
    +				// `matching` function tests parameter
    +				// against passed regular expression.
    +				parameter 'offset': value(consumer(matching("[0-9]+")), producer(123))
    +
    +				// `notMatching` functions tests if parameter
    +				// does not match passed regular expression.
    +				parameter 'loginStartsWith': value(consumer(notMatching(".{0,2}")), producer(3))
    +			}
    +		}
    +
    +		//...
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +  queryParameters:
    +    a: b
    +    b: c
    +  headers:
    +    foo: bar
    +    fooReq: baz
    +  cookies:
    +    foo: bar
    +    fooReq: baz
    +  body:
    +    foo: bar
    +  matchers:
    +    body:
    +      - path: $.foo
    +        type: by_regex
    +        value: bar
    +    headers:
    +      - key: foo
    +        regex: bar
    +response:
    +  status: 200
    +  fixedDelayMilliseconds: 1000
    +  headers:
    +    foo2: bar
    +    foo3: foo33
    +    fooRes: baz
    +  body:
    +    foo2: bar
    +    foo3: baz
    +    nullValue: null
    +  matchers:
    +    body:
    +      - path: $.foo2
    +        type: by_regex
    +        value: bar
    +      - path: $.foo3
    +        type: by_command
    +        value: executeMe($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +    headers:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        command: andMeToo($it)
    +    cookies:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        predefined:

    +

    request may contain additional request headers, as shown in the following example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +
    +		// Each header is added in form `'Header-Name' : 'Header-Value'`.
    +		// there are also some helper methods
    +		headers {
    +			header 'key': 'value'
    +			contentType(applicationJson())
    +		}
    +
    +		//...
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +headers:
    +  foo: bar
    +  fooReq: baz

    +

    request may contain additional request cookies, as shown in the following example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +
    +		// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
    +		// there are also some helper methods
    +		cookies {
    +			cookie 'key': 'value'
    +			cookie('another_key', 'another_value')
    +		}
    +
    +		//...
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +cookies:
    +  foo: bar
    +  fooReq: baz

    +

    request may contain a request body:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +
    +		// Currently only JSON format of request body is supported.
    +		// Format will be determined from a header or body's content.
    +		body '''{ "login" : "john", "name": "John The Contract" }'''
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +body:
    +  foo: bar

    +

    request may contain multipart elements. To include multipart elements, use the +multipart method/section, as shown in the following examples

    Groovy DSL.  +

    +

    YAML.  +

    request:
    +  method: PUT
    +  url: /multipart
    +  headers:
    +    Content-Type: multipart/form-data;boundary=AaB03x
    +  multipart:
    +    params:
    +      # key (parameter name), value (parameter value) pair
    +      formParameter: '"formParameterValue"'
    +      someBooleanParameter: true
    +    named:
    +      - paramName: file
    +        fileName: filename.csv
    +        fileContent: file content
    +  matchers:
    +    multipart:
    +      params:
    +        - key: formParameter
    +          regex: ".+"
    +        - key: someBooleanParameter
    +          predefined: any_boolean
    +      named:
    +        - paramName: file
    +          fileName:
    +            predefined: non_empty
    +          fileContent:
    +            predefined: non_empty
    +response:
    +  status: 200

    +

    In the preceding example, we define parameters in either of two ways:

    Groovy DSL

    • Directly, by using the map notation, where the value can be a dynamic property (such as +formParameter: $(consumer(…​), producer(…​))).
    • By using the named(…​) method that lets you set a named parameter. A named parameter +can set a name and content. You can call it either via a method with two arguments, +such as named("fileName", "fileContent"), or via a map notation, such as +named(name: "fileName", content: "fileContent").

    YAML

    • The multipart parameters are set via multipart.params section
    • The named parameters (the fileName and fileContent for a given parameter name) +can be set via the multipart.named section. That section contains +the paramName (name of the parameter), fileName (name of the file), +fileContent (content of the file) fields
    • The dynamic bits can be set via the matchers.multipart section

      • for parameters use the params section that can accept +regex or a predefined regular expression
      • for named params use the named section where first you +define the parameter name via paramName and then you can pass the +parametrization of either fileName or fileContent via +regex or a predefined regular expression

    From this contract, the generated test is as follows:

    // given:
    + MockMvcRequestSpecification request = given()
    +   .header("Content-Type", "multipart/form-data;boundary=AaB03x")
    +   .param("formParameter", "\"formParameterValue\"")
    +   .param("someBooleanParameter", "true")
    +   .multiPart("file", "filename.csv", "file content".getBytes());
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .put("/multipart");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);

    The WireMock stub is as follows:

    			'''
    +{
    +  "request" : {
    +	"url" : "/multipart",
    +	"method" : "PUT",
    +	"headers" : {
    +	  "Content-Type" : {
    +		"matches" : "multipart/form-data;boundary=AaB03x.*"
    +	  }
    +	},
    +	"bodyPatterns" : [ {
    +		"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n\\".+\\"\\r\\n--\\\\1.*"
    +  		}, {
    +    			"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n(true|false)\\r\\n--\\\\1.*"
    +  		}, {
    +	  "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n[\\\\S\\\\s]+\\r\\n--\\\\1.*"
    +	} ]
    +  },
    +  "response" : {
    +	"status" : 200,
    +	"transformers" : [ "response-template", "foo-transformer" ]
    +  }
    +}
    +	'''

    94.4 Response

    The response must contain an HTTP status code and may contain other information. The +following code shows an example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +	}
    +	response {
    +		// Status code sent by the server
    +		// in response to request specified above.
    +		status OK()
    +	}
    +}

    +

    YAML.  +

    response:
    +...
    +status: 200

    +

    Besides status, the response may contain headers, cookies and a body, both of which are +specified the same way as in the request (see the previous paragraph).

    [Tip]Tip

    Via the Groovy DSL you can reference the org.springframework.cloud.contract.spec.internal.HttpStatus +methods to provide a meaningful status instead of a digit. E.g. you can call +OK() for a status 200 or BAD_REQUEST() for 400.

    94.5 Dynamic properties

    The contract can contain some dynamic properties: timestamps, IDs, and so on. You do not +want to force the consumers to stub their clocks to always return the same value of time +so that it gets matched by the stub.

    For Groovy DSL you can provide the dynamic parts in your contracts +in two ways: pass them directly in the body or set them in a separate section called +bodyMatchers.

    [Note]Note

    Before 2.0.0 these were set using testMatchers and stubMatchers, +check out the migration guide for more information.

    For YAML you can only use the matchers section.

    94.5.1 Dynamic properties inside the body

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    You can set the properties inside the body either with the value method or, if you use +the Groovy map notation, with $(). The following example shows how to set dynamic +properties with the value method:

    value(consumer(...), producer(...))
    +value(c(...), p(...))
    +value(stub(...), test(...))
    +value(client(...), server(...))

    The following example shows how to set dynamic properties with $():

    $(consumer(...), producer(...))
    +$(c(...), p(...))
    +$(stub(...), test(...))
    +$(client(...), server(...))

    Both approaches work equally well. stub and client methods are aliases over the consumer +method. Subsequent sections take a closer look at what you can do with those values.

    94.5.2 Regular expressions

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    You can use regular expressions to write your requests in Contract DSL. Doing so is +particularly useful when you want to indicate that a given response should be provided +for requests that follow a given pattern. Also, you can use regular expressions when you +need to use patterns and not exact values both for your test and your server side tests.

    The following example shows how to use regular expressions to write a request:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method('GET')
    +		url $(consumer(~/\/[0-9]{2}/), producer('/12'))
    +	}
    +	response {
    +		status OK()
    +		body(
    +				id: $(anyNumber()),
    +				surname: $(
    +						consumer('Kowalsky'),
    +						producer(regex('[a-zA-Z]+'))
    +				),
    +				name: 'Jan',
    +				created: $(consumer('2014-02-02 12:23:43'), producer(execute('currentDate(it)'))),
    +				correlationId: value(consumer('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
    +						producer(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
    +				)
    +		)
    +		headers {
    +			header 'Content-Type': 'text/plain'
    +		}
    +	}
    +}

    You can also provide only one side of the communication with a regular expression. If you +do so, then the contract engine automatically provides the generated string that matches +the provided regular expression. The following code shows an example:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'PUT'
    +		url value(consumer(regex('/foo/[0-9]{5}')))
    +		body([
    +				requestElement: $(consumer(regex('[0-9]{5}')))
    +		])
    +		headers {
    +			header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*'))))
    +		}
    +	}
    +	response {
    +		status OK()
    +		body([
    +				responseElement: $(producer(regex('[0-9]{7}')))
    +		])
    +		headers {
    +			contentType("application/vnd.fraud.v1+json")
    +		}
    +	}
    +}

    In the preceding example, the opposite side of the communication has the respective data +generated for request and response.

    Spring Cloud Contract comes with a series of predefined regular expressions that you can +use in your contracts, as shown in the following example:

    protected static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/)
    +protected static final Pattern ALPHA_NUMERIC = Pattern.compile('[a-zA-Z0-9]+')
    +protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/)
    +protected static final Pattern NUMBER = Pattern.compile('-?(\\d*\\.\\d+|\\d+)')
    +protected static final Pattern INTEGER = Pattern.compile('-?(\\d+)')
    +protected static final Pattern POSITIVE_INT = Pattern.compile('([1-9]\\d*)')
    +protected static final Pattern DOUBLE = Pattern.compile('-?(\\d*\\.\\d+)')
    +protected static final Pattern HEX = Pattern.compile('[a-fA-F0-9]+')
    +protected static final Pattern IP_ADDRESS = Pattern.
    +		compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])')
    +protected static final Pattern HOSTNAME_PATTERN = Pattern.
    +		compile('((http[s]?|ftp):/)/?([^:/\\s]+)(:[0-9]{1,5})?')
    +protected static final Pattern EMAIL = Pattern.
    +		compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}')
    +protected static final Pattern URL = UrlHelper.URL
    +protected static final Pattern HTTPS_URL = UrlHelper.HTTPS_URL
    +protected static final Pattern UUID = Pattern.
    +		compile('[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}')
    +protected static final Pattern ANY_DATE = Pattern.
    +		compile('(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])')
    +protected static final Pattern ANY_DATE_TIME = Pattern.
    +		compile('([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])')
    +protected static final Pattern ANY_TIME = Pattern.
    +		compile('(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])')
    +protected static final Pattern NON_EMPTY = Pattern.compile(/[\S\s]+/)
    +protected static final Pattern NON_BLANK = Pattern.compile(/^\s*\S[\S\s]*/)
    +protected static final Pattern ISO8601_WITH_OFFSET = Pattern.
    +		compile(/([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.\d{1,6})?(Z|[+-][01]\d:[0-5]\d)/)
    +
    +protected static Pattern anyOf(String... values) {
    +	return Pattern.compile(values.collect({ "^$it\$" }).join("|"))
    +}
    +
    +RegexProperty onlyAlphaUnicode() {
    +	return new RegexProperty(ONLY_ALPHA_UNICODE).asString()
    +}
    +
    +RegexProperty alphaNumeric() {
    +	return new RegexProperty(ALPHA_NUMERIC).asString()
    +}
    +
    +RegexProperty number() {
    +	return new RegexProperty(NUMBER).asDouble()
    +}
    +
    +RegexProperty positiveInt() {
    +	return new RegexProperty(POSITIVE_INT).asInteger()
    +}
    +
    +RegexProperty anyBoolean() {
    +	return new RegexProperty(TRUE_OR_FALSE).asBooleanType()
    +}
    +
    +RegexProperty anInteger() {
    +	return new RegexProperty(INTEGER).asInteger()
    +}
    +
    +RegexProperty aDouble() {
    +	return new RegexProperty(DOUBLE).asDouble()
    +}
    +
    +RegexProperty ipAddress() {
    +	return new RegexProperty(IP_ADDRESS).asString()
    +}
    +
    +RegexProperty hostname() {
    +	return new RegexProperty(HOSTNAME_PATTERN).asString()
    +}
    +
    +RegexProperty email() {
    +	return new RegexProperty(EMAIL).asString()
    +}
    +
    +RegexProperty url() {
    +	return new RegexProperty(URL).asString()
    +}
    +
    +RegexProperty httpsUrl() {
    +	return new RegexProperty(HTTPS_URL).asString()
    +}
    +
    +RegexProperty uuid() {
    +	return new RegexProperty(UUID).asString()
    +}
    +
    +RegexProperty isoDate() {
    +	return new RegexProperty(ANY_DATE).asString()
    +}
    +
    +RegexProperty isoDateTime() {
    +	return new RegexProperty(ANY_DATE_TIME).asString()
    +}
    +
    +RegexProperty isoTime() {
    +	return new RegexProperty(ANY_TIME).asString()
    +}
    +
    +RegexProperty iso8601WithOffset() {
    +	return new RegexProperty(ISO8601_WITH_OFFSET).asString()
    +}
    +
    +RegexProperty nonEmpty() {
    +	return new RegexProperty(NON_EMPTY).asString()
    +}
    +
    +RegexProperty nonBlank() {
    +	return new RegexProperty(NON_BLANK).asString()
    +}

    In your contract, you can use it as shown in the following example:

    Contract dslWithOptionalsInString = Contract.make {
    +	priority 1
    +	request {
    +		method POST()
    +		url '/users/password'
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(
    +				email: $(consumer(optional(regex(email()))), producer('abc@abc.com')),
    +				callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
    +		)
    +	}
    +	response {
    +		status 404
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(
    +				code: value(consumer("123123"), producer(optional("123123"))),
    +				message: "User not found by email = [${value(producer(regex(email())), consumer('not.existing@user.com'))}]"
    +		)
    +	}
    +}

    To make matters even simpler you can use a set of predefined objects that will automatically assume that you want a regular expression to be passed. +All of those methods start with any prefix:

    T anyAlphaUnicode()
    +
    +T anyAlphaNumeric()
    +
    +T anyNumber()
    +
    +T anyInteger()
    +
    +T anyPositiveInt()
    +
    +T anyDouble()
    +
    +T anyHex()
    +
    +T aBoolean()
    +
    +T anyIpAddress()
    +
    +T anyHostname()
    +
    +T anyEmail()
    +
    +T anyUrl()
    +
    +T anyHttpsUrl()
    +
    +T anyUuid()
    +
    +T anyDate()
    +
    +T anyDateTime()
    +
    +T anyTime()
    +
    +T anyIso8601WithOffset()
    +
    +T anyNonBlankString()
    +
    +T anyNonEmptyString()
    +
    +T anyOf(String... values)

    and this is an example of how you can reference those methods:

    Contract contractDsl = Contract.make {
    +	label 'trigger_event'
    +	input {
    +		triggeredBy('toString()')
    +	}
    +	outputMessage {
    +		sentTo 'topic.rateablequote'
    +		body([
    +				alpha            : $(anyAlphaUnicode()),
    +				number           : $(anyNumber()),
    +				anInteger        : $(anyInteger()),
    +				positiveInt      : $(anyPositiveInt()),
    +				aDouble          : $(anyDouble()),
    +				aBoolean         : $(aBoolean()),
    +				ip               : $(anyIpAddress()),
    +				hostname         : $(anyHostname()),
    +				email            : $(anyEmail()),
    +				url              : $(anyUrl()),
    +				httpsUrl         : $(anyHttpsUrl()),
    +				uuid             : $(anyUuid()),
    +				date             : $(anyDate()),
    +				dateTime         : $(anyDateTime()),
    +				time             : $(anyTime()),
    +				iso8601WithOffset: $(anyIso8601WithOffset()),
    +				nonBlankString   : $(anyNonBlankString()),
    +				nonEmptyString   : $(anyNonEmptyString()),
    +				anyOf            : $(anyOf('foo', 'bar'))
    +		])
    +	}
    +}

    94.5.3 Passing Optional Parameters

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    It is possible to provide optional parameters in your contract. However, you can provide +optional parameters only for the following:

    • STUB side of the Request
    • TEST side of the Response

    The following example shows how to provide optional parameters:

    org.springframework.cloud.contract.spec.Contract.make {
    +	priority 1
    +	request {
    +		method 'POST'
    +		url '/users/password'
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(
    +				email: $(consumer(optional(regex(email()))), producer('abc@abc.com')),
    +				callback_url: $(consumer(regex(hostname())), producer('https://partners.com'))
    +		)
    +	}
    +	response {
    +		status 404
    +		headers {
    +			header 'Content-Type': 'application/json'
    +		}
    +		body(
    +				code: value(consumer("123123"), producer(optional("123123")))
    +		)
    +	}
    +}

    By wrapping a part of the body with the optional() method, you create a regular +expression that must be present 0 or more times.

    If you use Spock for, the following test would be generated from the previous example:

    					"""
    + given:
    +  def request = given()
    +    .header("Content-Type", "application/json")
    +    .body('''{"email":"abc@abc.com","callback_url":"https://partners.com"}''')
    +
    + when:
    +  def response = given().spec(request)
    +    .post("/users/password")
    +
    + then:
    +  response.statusCode == 404
    +  response.header('Content-Type')  == 'application/json'
    + and:
    +  DocumentContext parsedJson = JsonPath.parse(response.body.asString())
    +  assertThatJson(parsedJson).field("['code']").matches("(123123)?")
    +"""

    The following stub would also be generated:

    					'''
    +{
    +  "request" : {
    +	"url" : "/users/password",
    +	"method" : "POST",
    +	"bodyPatterns" : [ {
    +	  "matchesJsonPath" : "$[?(@.['email'] =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6})?/)]"
    +	}, {
    +	  "matchesJsonPath" : "$[?(@.['callback_url'] =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]"
    +	} ],
    +	"headers" : {
    +	  "Content-Type" : {
    +		"equalTo" : "application/json"
    +	  }
    +	}
    +  },
    +  "response" : {
    +	"status" : 404,
    +	"body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}",
    +	"headers" : {
    +	  "Content-Type" : "application/json"
    +	}
    +  },
    +  "priority" : 1
    +}
    +'''

    94.5.4 Executing Custom Methods on the Server Side

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    You can define a method call that executes on the server side during the test. Such a +method can be added to the class defined as "baseClassForTests" in the configuration. The +following code shows an example of the contract portion of the test case:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'PUT'
    +		url $(consumer(regex('^/api/[0-9]{2}$')), producer('/api/12'))
    +		headers {
    +			header 'Content-Type': 'application/json'
    +		}
    +		body '''\
    +			[{
    +				"text": "Gonna see you at Warsaw"
    +			}]
    +		'''
    +	}
    +	response {
    +		body(
    +				path: $(consumer('/api/12'), producer(regex('^/api/[0-9]{2}$'))),
    +				correlationId: $(consumer('1223456'), producer(execute('isProperCorrelationId($it)')))
    +		)
    +		status OK()
    +	}
    +}

    The following code shows the base class portion of the test case:

    abstract class BaseMockMvcSpec extends Specification {
    +
    +	def setup() {
    +		RestAssuredMockMvc.standaloneSetup(new PairIdController())
    +	}
    +
    +	void isProperCorrelationId(Integer correlationId) {
    +		assert correlationId == 123456
    +	}
    +
    +	void isEmpty(String value) {
    +		assert value == null
    +	}
    +
    +}
    [Important]Important

    You cannot use both a String and execute to perform concatenation. For +example, calling header('Authorization', 'Bearer ' + execute('authToken()')) leads to +improper results. Instead, call header('Authorization', execute('authToken()')) and +ensure that the authToken() method returns everything you need.

    The type of the object read from the JSON can be one of the following, depending on the +JSON path:

    • String: If you point to a String value in the JSON.
    • JSONArray: If you point to a List in the JSON.
    • Map: If you point to a Map in the JSON.
    • Number: If you point to Integer, Double etc. in the JSON.
    • Boolean: If you point to a Boolean in the JSON.

    In the request part of the contract, you can specify that the body should be taken from +a method.

    [Important]Important

    You must provide both the consumer and the producer side. The execute part +is applied for the whole body - not for parts of it.

    The following example shows how to read an object from JSON:

    Contract contractDsl = Contract.make {
    +	request {
    +		method 'GET'
    +		url '/something'
    +		body(
    +				$(c('foo'), p(execute('hashCode()')))
    +		)
    +	}
    +	response {
    +		status OK()
    +	}
    +}

    The preceding example results in calling the hashCode() method in the request body. +It should resemble the following code:

    // given:
    + MockMvcRequestSpecification request = given()
    +   .body(hashCode());
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .get("/something");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);

    94.5.5 Referencing the Request from the Response

    The best situation is to provide fixed values, but sometimes you need to reference a +request in your response.

    If you’re writing contracts using Groovy DSL, you can use the fromRequest() method, which lets +you reference a bunch of elements from the HTTP request. You can use the following +options:

    • fromRequest().url(): Returns the request URL and query parameters.
    • fromRequest().query(String key): Returns the first query parameter with a given name.
    • fromRequest().query(String key, int index): Returns the nth query parameter with a +given name.
    • fromRequest().path(): Returns the full path.
    • fromRequest().path(int index): Returns the nth path element.
    • fromRequest().header(String key): Returns the first header with a given name.
    • fromRequest().header(String key, int index): Returns the nth header with a given name.
    • fromRequest().body(): Returns the full request body.
    • fromRequest().body(String jsonPath): Returns the element from the request that +matches the JSON Path.

    If you’re using the YAML contract definition you have to use the +Handlebars {{{ }}} notation with custom, Spring Cloud Contract + functions to achieve this.

    • {{{ request.url }}}: Returns the request URL and query parameters.
    • {{{ request.query.key.[index] }}}: Returns the nth query parameter with a given name. +E.g. for key foo, first entry {{{ request.query.foo.[0] }}}
    • {{{ request.path }}}: Returns the full path.
    • {{{ request.path.[index] }}}: Returns the nth path element. E.g. +for first entry `{{{ request.path.[0] }}}
    • {{{ request.headers.key }}}: Returns the first header with a given name.
    • {{{ request.headers.key.[index] }}}: Returns the nth header with a given name.
    • {{{ request.body }}}: Returns the full request body.
    • {{{ jsonpath this 'your.json.path' }}}: Returns the element from the request that +matches the JSON Path. E.g. for json path $.foo - {{{ jsonpath this '$.foo' }}}

    Consider the following contract:

    Groovy DSL.  +

    +

    YAML.  +

    request:
    +  method: GET
    +  url: /api/v1/xxxx
    +  queryParameters:
    +    foo:
    +      - bar
    +      - bar2
    +  headers:
    +    Authorization:
    +      - secret
    +      - secret2
    +  body:
    +    foo: bar
    +    baz: 5
    +response:
    +  status: 200
    +  headers:
    +    Authorization: "foo {{{ request.headers.Authorization.0 }}} bar"
    +  body:
    +    url: "{{{ request.url }}}"
    +    path: "{{{ request.path }}}"
    +    pathIndex: "{{{ request.path.1 }}}"
    +    param: "{{{ request.query.foo }}}"
    +    paramIndex: "{{{ request.query.foo.1 }}}"
    +    authorization: "{{{ request.headers.Authorization.0 }}}"
    +    authorization2: "{{{ request.headers.Authorization.1 }}"
    +    fullBody: "{{{ request.body }}}"
    +    responseFoo: "{{{ jsonpath this '$.foo' }}}"
    +    responseBaz: "{{{ jsonpath this '$.baz' }}}"
    +    responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla"

    +

    Running a JUnit test generation leads to a test that resembles the following example:

    // given:
    + MockMvcRequestSpecification request = given()
    +   .header("Authorization", "secret")
    +   .header("Authorization", "secret2")
    +   .body("{\"foo\":\"bar\",\"baz\":5}");
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .queryParam("foo","bar")
    +   .queryParam("foo","bar2")
    +   .get("/api/v1/xxxx");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);
    + assertThat(response.header("Authorization")).isEqualTo("foo secret bar");
    +// and:
    + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    + assertThatJson(parsedJson).field("['fullBody']").isEqualTo("{\"foo\":\"bar\",\"baz\":5}");
    + assertThatJson(parsedJson).field("['authorization']").isEqualTo("secret");
    + assertThatJson(parsedJson).field("['authorization2']").isEqualTo("secret2");
    + assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx");
    + assertThatJson(parsedJson).field("['param']").isEqualTo("bar");
    + assertThatJson(parsedJson).field("['paramIndex']").isEqualTo("bar2");
    + assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1");
    + assertThatJson(parsedJson).field("['responseBaz']").isEqualTo(5);
    + assertThatJson(parsedJson).field("['responseFoo']").isEqualTo("bar");
    + assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2");
    + assertThatJson(parsedJson).field("['responseBaz2']").isEqualTo("Bla bla bar bla bla");

    As you can see, elements from the request have been properly referenced in the response.

    The generated WireMock stub should resemble the following example:

    {
    +  "request" : {
    +    "urlPath" : "/api/v1/xxxx",
    +    "method" : "POST",
    +    "headers" : {
    +      "Authorization" : {
    +        "equalTo" : "secret2"
    +      }
    +    },
    +    "queryParameters" : {
    +      "foo" : {
    +        "equalTo" : "bar2"
    +      }
    +    },
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$[?(@.['baz'] == 5)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.['foo'] == 'bar')]"
    +    } ]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "{\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"path\":\"{{{request.path}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"param\":\"{{{request.query.foo.[0]}}}\",\"pathIndex\":\"{{{request.path.[1]}}}\",\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"url\":\"{{{request.url}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\"}",
    +    "headers" : {
    +      "Authorization" : "{{{request.headers.Authorization.[0]}}};foo"
    +    },
    +    "transformers" : [ "response-template" ]
    +  }
    +}

    Sending a request such as the one presented in the request part of the contract results +in sending the following response body:

    {
    +  "url" : "/api/v1/xxxx?foo=bar&foo=bar2",
    +  "path" : "/api/v1/xxxx",
    +  "pathIndex" : "v1",
    +  "param" : "bar",
    +  "paramIndex" : "bar2",
    +  "authorization" : "secret",
    +  "authorization2" : "secret2",
    +  "fullBody" : "{\"foo\":\"bar\",\"baz\":5}",
    +  "responseFoo" : "bar",
    +  "responseBaz" : 5,
    +  "responseBaz2" : "Bla bla bar bla bla"
    +}
    [Important]Important

    This feature works only with WireMock having a version greater than or equal +to 2.5.1. The Spring Cloud Contract Verifier uses WireMock’s +response-template response transformer. It uses Handlebars to convert the Mustache {{{ }}} templates into +proper values. Additionally, it registers two helper functions:

    • escapejsonbody: Escapes the request body in a format that can be embedded in a JSON.
    • jsonpath: For a given parameter, find an object in the request body.

    94.5.6 Registering Your Own WireMock Extension

    WireMock lets you register custom extensions. By default, Spring Cloud Contract registers +the transformer, which lets you reference a request from a response. If you want to +provide your own extensions, you can register an implementation of the +org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions interface. +Since we use the spring.factories extension approach, you can create an entry in +META-INF/spring.factories file similar to the following:

    org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions=\
    +org.springframework.cloud.contract.stubrunner.provider.wiremock.TestWireMockExtensions
    +org.springframework.cloud.contract.spec.ContractConverter=\
    +org.springframework.cloud.contract.stubrunner.TestCustomYamlContractConverter

    The following is an example of a custom extension:

    TestWireMockExtensions.groovy.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.springframework.cloud.contract.verifier.dsl.wiremock
    +
    +import com.github.tomakehurst.wiremock.extension.Extension
    +
    +/**
    + * Extension that registers the default transformer and the custom one
    + */
    +class TestWireMockExtensions implements WireMockExtensions {
    +	@Override
    +	List<Extension> extensions() {
    +		return [
    +				new DefaultResponseTransformer(),
    +				new CustomExtension()
    +		]
    +	}
    +}
    +
    +class CustomExtension implements Extension {
    +
    +	@Override
    +	String getName() {
    +		return "foo-transformer"
    +	}
    +}

    +

    [Important]Important

    Remember to override the applyGlobally() method and set it to false if you +want the transformation to be applied only for a mapping that explicitly requires it.

    94.5.7 Dynamic Properties in the Matchers Sections

    If you work with Pact, the following discussion may seem familiar. +Quite a few users are used to having a separation between the body and setting the +dynamic parts of a contract.

    You can use the bodyMatchers section for two reasons:

    • Define the dynamic values that should end up in a stub. +You can set it in the request or inputMessage part of your contract.
    • Verify the result of your test. +This section is present in the response or outputMessage side of the +contract.

    Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the +following matching possibilities:

    Groovy DSL

    • For the stubs(in tests on the Consumer’s side):

      • byEquality(): The value taken from the consumer’s request via the provided JSON Path must be +equal to the value provided in the contract.
      • byRegex(…​): The value taken from the consumer’s request via the provided JSON Path must +match the regex. You can also pass the type of the expected matched value (e.g. asString(), asLong() etc.)
      • byDate(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO Date value.
      • byTimestamp(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO DateTime value.
      • byTime(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO Time value.
    • For the verification(in generated tests on the Producer’s side):

      • byEquality(): The value taken from the producer’s response via the provided JSON Path must be +equal to the provided value in the contract.
      • byRegex(…​): The value taken from the producer’s response via the provided JSON Path must +match the regex.
      • byDate(): The value taken from the producer’s response via the provided JSON Path must match +the regex for an ISO Date value.
      • byTimestamp(): The value taken from the producer’s response via the provided JSON Path must +match the regex for an ISO DateTime value.
      • byTime(): The value taken from the producer’s response via the provided JSON Path must match +the regex for an ISO Time value.
      • byType(): The value taken from the producer’s response via the provided JSON Path needs to be +of the same type as the type defined in the body of the response in the contract. +byType can take a closure, in which you can set minOccurrence and maxOccurrence. For the request side, you should use the closure to assert size of the collection. +That way, you can assert the size of the flattened collection. To check the size of an +unflattened collection, use a custom method with the byCommand(…​) testMatcher.
      • byCommand(…​): The value taken from the producer’s response via the provided JSON Path is +passed as an input to the custom method that you provide. For example, +byCommand('foo($it)') results in calling a foo method to which the value matching the +JSON Path gets passed. The type of the object read from the JSON can be one of the +following, depending on the JSON path:

        • String: If you point to a String value.
        • JSONArray: If you point to a List.
        • Map: If you point to a Map.
        • Number: If you point to Integer, Double, or other kind of number.
        • Boolean: If you point to a Boolean.
      • byNull(): The value taken from the response via the provided JSON Path must be null

    YAML. Please read the Groovy section for detailed explanation of +what the types mean

    For YAML the structure of a matcher looks like this

    - path: $.foo
    +  type: by_regex
    +  value: bar
    +  regexType: as_string

    Or if you want to use one of the predefined regular expressions +[only_alpha_unicode, number, any_boolean, ip_address, hostname, +email, url, uuid, iso_date, iso_date_time, iso_time, iso_8601_with_offset, non_empty, non_blank]:

    - path: $.foo
    +  type: by_regex
    +  predefined: only_alpha_unicode

    Below you can find the allowed list of `type`s.

    • For stubMatchers:

      • by_equality
      • by_regex
      • by_date
      • by_timestamp
      • by_time
      • by_type

        • there are 2 additional fields accepted: minOccurrence and maxOccurrence.
    • For testMatchers:

      • by_equality
      • by_regex
      • by_date
      • by_timestamp
      • by_time
      • by_type

        • there are 2 additional fields accepted: minOccurrence and maxOccurrence.
      • by_command
      • by_null

    You can also define which type the regular expression corresponds to via the regexType field. Below you can find the allowed list of regular expression types:

    • as_integer
    • as_double
    • as_float,
    • as_long
    • as_short
    • as_boolean
    • as_string

    Consider the following example:

    Groovy DSL.  +

    Contract contractDsl = Contract.make {
    +	request {
    +		method 'GET'
    +		urlPath '/get'
    +		body([
    +				duck                : 123,
    +				alpha               : 'abc',
    +				number              : 123,
    +				aBoolean            : true,
    +				date                : '2017-01-01',
    +				dateTime            : '2017-01-01T01:23:45',
    +				time                : '01:02:34',
    +				valueWithoutAMatcher: 'foo',
    +				valueWithTypeMatch  : 'string',
    +				key                 : [
    +						'complex.key': 'foo'
    +				]
    +		])
    +		bodyMatchers {
    +			jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
    +			jsonPath('$.duck', byEquality())
    +			jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
    +			jsonPath('$.alpha', byEquality())
    +			jsonPath('$.number', byRegex(number()).asInteger())
    +			jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
    +			jsonPath('$.date', byDate())
    +			jsonPath('$.dateTime', byTimestamp())
    +			jsonPath('$.time', byTime())
    +			jsonPath("\$.['key'].['complex.key']", byEquality())
    +		}
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +	response {
    +		status OK()
    +		body([
    +				duck                 : 123,
    +				alpha                : 'abc',
    +				number               : 123,
    +				positiveInteger      : 1234567890,
    +				negativeInteger      : -1234567890,
    +				positiveDecimalNumber: 123.4567890,
    +				negativeDecimalNumber: -123.4567890,
    +				aBoolean             : true,
    +				date                 : '2017-01-01',
    +				dateTime             : '2017-01-01T01:23:45',
    +				time                 : "01:02:34",
    +				valueWithoutAMatcher : 'foo',
    +				valueWithTypeMatch   : 'string',
    +				valueWithMin         : [
    +						1, 2, 3
    +				],
    +				valueWithMax         : [
    +						1, 2, 3
    +				],
    +				valueWithMinMax      : [
    +						1, 2, 3
    +				],
    +				valueWithMinEmpty    : [],
    +				valueWithMaxEmpty    : [],
    +				key                  : [
    +						'complex.key': 'foo'
    +				],
    +				nullValue            : null
    +		])
    +		bodyMatchers {
    +			// asserts the jsonpath value against manual regex
    +			jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
    +			// asserts the jsonpath value against the provided value
    +			jsonPath('$.duck', byEquality())
    +			// asserts the jsonpath value against some default regex
    +			jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
    +			jsonPath('$.alpha', byEquality())
    +			jsonPath('$.number', byRegex(number()).asInteger())
    +			jsonPath('$.positiveInteger', byRegex(anInteger()).asInteger())
    +			jsonPath('$.negativeInteger', byRegex(anInteger()).asInteger())
    +			jsonPath('$.positiveDecimalNumber', byRegex(aDouble()).asDouble())
    +			jsonPath('$.negativeDecimalNumber', byRegex(aDouble()).asDouble())
    +			jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
    +			// asserts vs inbuilt time related regex
    +			jsonPath('$.date', byDate())
    +			jsonPath('$.dateTime', byTimestamp())
    +			jsonPath('$.time', byTime())
    +			// asserts that the resulting type is the same as in response body
    +			jsonPath('$.valueWithTypeMatch', byType())
    +			jsonPath('$.valueWithMin', byType {
    +				// results in verification of size of array (min 1)
    +				minOccurrence(1)
    +			})
    +			jsonPath('$.valueWithMax', byType {
    +				// results in verification of size of array (max 3)
    +				maxOccurrence(3)
    +			})
    +			jsonPath('$.valueWithMinMax', byType {
    +				// results in verification of size of array (min 1 & max 3)
    +				minOccurrence(1)
    +				maxOccurrence(3)
    +			})
    +			jsonPath('$.valueWithMinEmpty', byType {
    +				// results in verification of size of array (min 0)
    +				minOccurrence(0)
    +			})
    +			jsonPath('$.valueWithMaxEmpty', byType {
    +				// results in verification of size of array (max 0)
    +				maxOccurrence(0)
    +			})
    +			// will execute a method `assertThatValueIsANumber`
    +			jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
    +			jsonPath("\$.['key'].['complex.key']", byEquality())
    +			jsonPath('$.nullValue', byNull())
    +		}
    +		headers {
    +			contentType(applicationJson())
    +			header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
    +		}
    +	}
    +}

    +

    YAML.  +

    request:
    +  method: GET
    +  urlPath: /get/1
    +  headers:
    +    Content-Type: application/json
    +  cookies:
    +    foo: 2
    +    bar: 3
    +  queryParameters:
    +    limit: 10
    +    offset: 20
    +    filter: 'email'
    +    sort: name
    +    search: 55
    +    age: 99
    +    name: John.Doe
    +    email: 'bob@email.com'
    +  body:
    +    duck: 123
    +    alpha: "abc"
    +    number: 123
    +    aBoolean: true
    +    date: "2017-01-01"
    +    dateTime: "2017-01-01T01:23:45"
    +    time: "01:02:34"
    +    valueWithoutAMatcher: "foo"
    +    valueWithTypeMatch: "string"
    +    key:
    +      "complex.key": 'foo'
    +    nullValue: null
    +    valueWithMin:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinEmpty: []
    +    valueWithMaxEmpty: []
    +  matchers:
    +    url:
    +      regex: /get/[0-9]
    +      # predefined:
    +      # execute a method
    +      #command: 'equals($it)'
    +    queryParameters:
    +      - key: limit
    +        type: equal_to
    +        value: 20
    +      - key: offset
    +        type: containing
    +        value: 20
    +      - key: sort
    +        type: equal_to
    +        value: name
    +      - key: search
    +        type: not_matching
    +        value: '^[0-9]{2}$'
    +      - key: age
    +        type: not_matching
    +        value: '^\\w*$'
    +      - key: name
    +        type: matching
    +        value: 'John.*'
    +      - key: hello
    +        type: absent
    +    cookies:
    +      - key: foo
    +        regex: '[0-9]'
    +      - key: bar
    +        command: 'equals($it)'
    +    headers:
    +      - key: Content-Type
    +        regex: "application/json.*"
    +    body:
    +      - path: $.duck
    +        type: by_regex
    +        value: "[0-9]{3}"
    +      - path: $.duck
    +        type: by_equality
    +      - path: $.alpha
    +        type: by_regex
    +        predefined: only_alpha_unicode
    +      - path: $.alpha
    +        type: by_equality
    +      - path: $.number
    +        type: by_regex
    +        predefined: number
    +      - path: $.aBoolean
    +        type: by_regex
    +        predefined: any_boolean
    +      - path: $.date
    +        type: by_date
    +      - path: $.dateTime
    +        type: by_timestamp
    +      - path: $.time
    +        type: by_time
    +      - path: "$.['key'].['complex.key']"
    +        type: by_equality
    +      - path: $.nullvalue
    +        type: by_null
    +      - path: $.valueWithMin
    +        type: by_type
    +        minOccurrence: 1
    +      - path: $.valueWithMax
    +        type: by_type
    +        maxOccurrence: 3
    +      - path: $.valueWithMinMax
    +        type: by_type
    +        minOccurrence: 1
    +        maxOccurrence: 3
    +response:
    +  status: 200
    +  cookies:
    +    foo: 1
    +    bar: 2
    +  body:
    +    duck: 123
    +    alpha: "abc"
    +    number: 123
    +    aBoolean: true
    +    date: "2017-01-01"
    +    dateTime: "2017-01-01T01:23:45"
    +    time: "01:02:34"
    +    valueWithoutAMatcher: "foo"
    +    valueWithTypeMatch: "string"
    +    valueWithMin:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinEmpty: []
    +    valueWithMaxEmpty: []
    +    key:
    +      'complex.key': 'foo'
    +    nulValue: null
    +  matchers:
    +    headers:
    +      - key: Content-Type
    +        regex: "application/json.*"
    +    cookies:
    +      - key: foo
    +        regex: '[0-9]'
    +      - key: bar
    +        command: 'equals($it)'
    +    body:
    +      - path: $.duck
    +        type: by_regex
    +        value: "[0-9]{3}"
    +      - path: $.duck
    +        type: by_equality
    +      - path: $.alpha
    +        type: by_regex
    +        predefined: only_alpha_unicode
    +      - path: $.alpha
    +        type: by_equality
    +      - path: $.number
    +        type: by_regex
    +        predefined: number
    +      - path: $.aBoolean
    +        type: by_regex
    +        predefined: any_boolean
    +      - path: $.date
    +        type: by_date
    +      - path: $.dateTime
    +        type: by_timestamp
    +      - path: $.time
    +        type: by_time
    +      - path: $.valueWithTypeMatch
    +        type: by_type
    +      - path: $.valueWithMin
    +        type: by_type
    +        minOccurrence: 1
    +      - path: $.valueWithMax
    +        type: by_type
    +        maxOccurrence: 3
    +      - path: $.valueWithMinMax
    +        type: by_type
    +        minOccurrence: 1
    +        maxOccurrence: 3
    +      - path: $.valueWithMinEmpty
    +        type: by_type
    +        minOccurrence: 0
    +      - path: $.valueWithMaxEmpty
    +        type: by_type
    +        maxOccurrence: 0
    +      - path: $.duck
    +        type: by_command
    +        value: assertThatValueIsANumber($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +  headers:
    +    Content-Type: application/json

    +

    In the preceding example, you can see the dynamic portions of the contract in the +matchers sections. For the request part, you can see that, for all fields but +valueWithoutAMatcher, the values of the regular expressions that the stub should +contain are explicitly set. For the valueWithoutAMatcher, the verification takes place +in the same way as without the use of matchers. In that case, the test performs an +equality check.

    For the response side in the bodyMatchers section, we define the dynamic parts in a +similar manner. The only difference is that the byType matchers are also present. The +verifier engine checks four fields to verify whether the response from the test +has a value for which the JSON path matches the given field, is of the same type as the one +defined in the response body, and passes the following check (based on the method being called):

    • For $.valueWithTypeMatch, the engine checks whether the type is the same.
    • For $.valueWithMin, the engine check the type and asserts whether the size is greater +than or equal to the minimum occurrence.
    • For $.valueWithMax, the engine checks the type and asserts whether the size is +smaller than or equal to the maximum occurrence.
    • For $.valueWithMinMax, the engine checks the type and asserts whether the size is +between the min and maximum occurrence.

    The resulting test would resemble the following example (note that an and section +separates the autogenerated assertions and the assertion from matchers):

    // given:
    + MockMvcRequestSpecification request = given()
    +   .header("Content-Type", "application/json")
    +   .body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\",\"key\":{\"complex.key\":\"foo\"}}");
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .get("/get");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);
    + assertThat(response.header("Content-Type")).matches("application/json.*");
    +// and:
    + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    + assertThatJson(parsedJson).field("['valueWithoutAMatcher']").isEqualTo("foo");
    +// and:
    + assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
    + assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
    + assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
    + assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
    + assertThat(parsedJson.read("$.number", String.class)).matches("-?(\\d*\\.\\d+|\\d+)");
    + assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
    + assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
    + assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
    + assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
    + assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
    + assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).as("$.valueWithMin").hasSizeGreaterThanOrEqualTo(1);
    + assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).as("$.valueWithMax").hasSizeLessThanOrEqualTo(3);
    + assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).as("$.valueWithMinMax").hasSizeBetween(1, 3);
    + assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).as("$.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0);
    + assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0);
    + assertThatValueIsANumber(parsedJson.read("$.duck"));
    + assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo");
    [Important]Important

    Notice that, for the byCommand method, the example calls the +assertThatValueIsANumber. This method must be defined in the test base class or be +statically imported to your tests. Notice that the byCommand call was converted to +assertThatValueIsANumber(parsedJson.read("$.duck"));. That means that the engine took +the method name and passed the proper JSON path as a parameter to it.

    The resulting WireMock stub is in the following example:

    					'''
    +{
    +  "request" : {
    +    "urlPath" : "/get",
    +    "method" : "POST",
    +    "headers" : {
    +      "Content-Type" : {
    +        "matches" : "application/json.*"
    +      }
    +    },
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]"
    +    }, {
    +      "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]"
    +    }, {
    +      "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.duck == 123)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.alpha == 'abc')]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\d+|\\\\d+))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.dateTime =~ /(([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
    +    }, {
    +      "matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithMin.size() >= 1)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithMax.size() <= 3)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithMinMax.size() >= 1 && @.valueWithMinMax.size() <= 3)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithOccurrence.size() >= 4 && @.valueWithOccurrence.size() <= 4)]"
    +    } ]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "{\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"number\\":123,\\"aBoolean\\":true,\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"time\\":\\"01:02:34\\",\\"valueWithoutAMatcher\\":\\"foo\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMin\\":[1,2,3],\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithOccurrence\\":[1,2,3,4]}",
    +    "headers" : {
    +      "Content-Type" : "application/json"
    +    },
    +    "transformers" : [ "response-template" ]
    +  }
    +}
    +'''
    [Important]Important

    If you use a matcher, then the part of the request and response that the +matcher addresses with the JSON Path gets removed from the assertion. In the case of +verifying a collection, you must create matchers for all the elements of the +collection.

    Consider the following example:

    Contract.make {
    +    request {
    +        method 'GET'
    +        url("/foo")
    +    }
    +    response {
    +        status OK()
    +        body(events: [[
    +                                 operation          : 'EXPORT',
    +                                 eventId            : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
    +                                 status             : 'OK'
    +                         ], [
    +                                 operation          : 'INPUT_PROCESSING',
    +                                 eventId            : '3bb4ac82-6652-462f-b6d1-75e424a0024a',
    +                                 status             : 'OK'
    +                         ]
    +                ]
    +        )
    +        bodyMatchers {
    +            jsonPath('$.events[0].operation', byRegex('.+'))
    +            jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$'))
    +            jsonPath('$.events[0].status', byRegex('.+'))
    +        }
    +    }
    +}

    The preceding code leads to creating the following test (the code block shows only the assertion section):

    and:
    +	DocumentContext parsedJson = JsonPath.parse(response.body.asString())
    +	assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("16f1ed75-0bcc-4f0d-a04d-3121798faf99")
    +	assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("EXPORT")
    +	assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("INPUT_PROCESSING")
    +	assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("3bb4ac82-6652-462f-b6d1-75e424a0024a")
    +	assertThatJson(parsedJson).array("['events']").contains("['status']").isEqualTo("OK")
    +and:
    +	assertThat(parsedJson.read("\$.events[0].operation", String.class)).matches(".+")
    +	assertThat(parsedJson.read("\$.events[0].eventId", String.class)).matches("^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\$")
    +	assertThat(parsedJson.read("\$.events[0].status", String.class)).matches(".+")

    As you can see, the assertion is malformed. Only the first element of the array got +asserted. In order to fix this, you should apply the assertion to the whole $.events +collection and assert it with the byCommand(…​) method.

    94.6 JAX-RS Support

    The Spring Cloud Contract Verifier supports the JAX-RS 2 Client API. The base class needs +to define protected WebTarget webTarget and server initialization. The only option for +testing JAX-RS API is to start a web server. Also, a request with a body needs to have a +content type set. Otherwise, the default of application/octet-stream gets used.

    In order to use JAX-RS mode, use the following settings:

    testMode == 'JAXRSCLIENT'

    The following example shows a generated test API:

    					'''
    + // when:
    +  Response response = webTarget
    +    .path("/users")
    +    .queryParam("limit", "10")
    +    .queryParam("offset", "20")
    +    .queryParam("filter", "email")
    +    .queryParam("sort", "name")
    +    .queryParam("search", "55")
    +    .queryParam("age", "99")
    +    .queryParam("name", "Denis.Stepanov")
    +    .queryParam("email", "bob@email.com")
    +    .request()
    +    .method("GET");
    +
    +  String responseAsString = response.readEntity(String.class);
    +
    + // then:
    +  assertThat(response.getStatus()).isEqualTo(200);
    + // and:
    +  DocumentContext parsedJson = JsonPath.parse(responseAsString);
    +  assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
    +'''

    94.7 Async Support

    If you’re using asynchronous communication on the server side (your controllers are +returning Callable, DeferredResult, and so on), then, inside your contract, you must +provide an async() method in the response section. The following code shows an example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +    request {
    +        method GET()
    +        url '/get'
    +    }
    +    response {
    +        status OK()
    +        body 'Passed'
    +        async()
    +    }
    +}

    +

    YAML.  +

    response:
    +    async: true

    +

    You can also use the fixedDelayMilliseconds method / property to add delay to your stubs.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +    request {
    +        method GET()
    +        url '/get'
    +    }
    +    response {
    +        status 200
    +        body 'Passed'
    +        fixedDelayMilliseconds 1000
    +    }
    +}

    +

    YAML.  +

    response:
    +    fixedDelayMilliseconds: 1000

    +

    94.8 Working with Context Paths

    Spring Cloud Contract supports context paths.

    [Important]Important

    The only change needed to fully support context paths is the switch on the +PRODUCER side. Also, the autogenerated tests must use EXPLICIT mode. The consumer +side remains untouched. In order for the generated test to pass, you must use EXPLICIT +mode.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <testMode>EXPLICIT</testMode>
    +    </configuration>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		testMode = 'EXPLICIT'
    +}

    +

    That way, you generate a test that DOES NOT use MockMvc. It means that you generate +real requests and you need to setup your generated test’s base class to work on a real +socket.

    Consider the following contract:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'GET'
    +		url '/my-context-path/url'
    +	}
    +	response {
    +		status OK()
    +	}
    +}

    The following example shows how to set up a base class and Rest Assured:

    import io.restassured.RestAssured;
    +import org.junit.Before;
    +import org.springframework.boot.web.server.LocalServerPort;
    +import org.springframework.boot.test.context.SpringBootTest;
    +
    +@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    +class ContextPathTestingBaseClass {
    +
    +	@LocalServerPort int port;
    +
    +	@Before
    +	public void setup() {
    +		RestAssured.baseURI = "http://localhost";
    +		RestAssured.port = this.port;
    +	}
    +}

    If you do it this way:

    • All of your requests in the autogenerated tests are sent to the real endpoint with your +context path included (for example, /my-context-path/url).
    • Your contracts reflect that you have a context path. Your generated stubs also have +that information (for example, in the stubs, you have to call /my-context-path/url).

    94.9 Working with WebFlux

    Spring Cloud Contract offers two ways of working with WebFlux.

    94.9.1 WebFlux with WebTestClient

    One of them is via the WebTestClient mode.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <testMode>WEBTESTCLIENT</testMode>
    +    </configuration>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		testMode = 'WEBTESTCLIENT'
    +}

    +

    The following example shows how to set up a WebTestClient base class and RestAssured +for WebFlux:

    import io.restassured.module.webtestclient.RestAssuredWebTestClient;
    +import org.junit.Before;
    +
    +public abstract class BeerRestBase {
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredWebTestClient.standaloneSetup(
    +		new ProducerController(personToCheck -> personToCheck.age >= 20));
    +	}
    +}
    +}

    94.9.2 WebFlux with Explicit mode

    Another way is with the EXPLICIT mode in your generated tests +to work with WebFlux.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <testMode>EXPLICIT</testMode>
    +    </configuration>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		testMode = 'EXPLICIT'
    +}

    +

    The following example shows how to set up a base class and Rest Assured for Web Flux:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(classes = BeerRestBase.Config.class,
    +		webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    +		properties = "server.port=0")
    +public abstract class BeerRestBase {
    +
    +    // your tests go here
    +
    +    // in this config class you define all controllers and mocked services
    +@Configuration
    +@EnableAutoConfiguration
    +static class Config {
    +
    +	@Bean
    +	PersonCheckingService personCheckingService()  {
    +		return personToCheck -> personToCheck.age >= 20;
    +	}
    +
    +	@Bean
    +	ProducerController producerController() {
    +		return new ProducerController(personCheckingService());
    +	}
    +}
    +
    +}

    94.10 XML Support for REST

    For REST contracts, we also support XML request and response body. +The XML body has to be passed within the body element +as a String or GString. Also body matchers can be provided for +both request and response. In place of the jsonPath(…​) method, the org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath +method should be used, with the desired xPath provided as the first argument +and the appropriate MatchingType as second. All the body matchers apart from byType() are supported.

    Here is an example of a Groovy DSL contract with XML response body:

    					Contract.make {
    +						request {
    +							method GET()
    +							urlPath '/get'
    +							headers {
    +								contentType(applicationXml())
    +							}
    +						}
    +						response {
    +							status(OK())
    +							headers {
    +								contentType(applicationXml())
    +							}
    +							body """
    +<test>
    +<duck type='xtype'>123</duck>
    +<alpha>abc</alpha>
    +<list>
    +<elem>abc</elem>
    +<elem>def</elem>
    +<elem>ghi</elem>
    +</list>
    +<number>123</number>
    +<aBoolean>true</aBoolean>
    +<date>2017-01-01</date>
    +<dateTime>2017-01-01T01:23:45</dateTime>
    +<time>01:02:34</time>
    +<valueWithoutAMatcher>foo</valueWithoutAMatcher>
    +<key><complex>foo</complex></key>
    +</test>"""
    +							bodyMatchers {
    +								xPath('/test/duck/text()', byRegex("[0-9]{3}"))
    +								xPath('/test/duck/text()', byCommand('test($it)'))
    +								xPath('/test/duck/xxx', byNull())
    +								xPath('/test/duck/text()', byEquality())
    +								xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
    +								xPath('/test/alpha/text()', byEquality())
    +								xPath('/test/number/text()', byRegex(number()))
    +								xPath('/test/date/text()', byDate())
    +								xPath('/test/dateTime/text()', byTimestamp())
    +								xPath('/test/time/text()', byTime())
    +								xPath('/test/*/complex/text()', byEquality())
    +								xPath('/test/duck/@type', byEquality())
    +							}
    +						}
    +					}

    And below is an example of a YAML contract with XML request and response bodies:

    include::{verifier_core_path}/src/test/resources/yml/contract_rest_xml.yml

    Here is an example of an automatically generated test for XML response body:

    @Test
    +public void validate_xmlMatches() throws Exception {
    +	// given:
    +	MockMvcRequestSpecification request = given()
    +				.header("Content-Type", "application/xml");
    +
    +	// when:
    +	ResponseOptions response = given().spec(request).get("/get");
    +
    +	// then:
    +	assertThat(response.statusCode()).isEqualTo(200);
    +	// and:
    +	DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance()
    +					.newDocumentBuilder();
    +	Document parsedXml = documentBuilder.parse(new InputSource(
    +				new StringReader(response.getBody().asString())));
    +	// and:
    +	assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc");
    +	assertThat(valueFromXPath(parsedXml,"/test/list/elem[2]/text()")).isEqualTo("def");
    +	assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]{3}");
    +	assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull();
    +	assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\p{L}]*");
    +	assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo");
    +	assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype");
    +	}

    94.11 Messaging Top-Level Elements

    The DSL for messaging looks a little bit different than the one that focuses on HTTP. The +following sections explain the differences:

    94.11.1 Output Triggered by a Method

    The output message can be triggered by calling a method (such as a Scheduler when a was +started and a message was sent), as shown in the following example:

    Groovy DSL.  +

    def dsl = Contract.make {
    +	// Human readable description
    +	description 'Some description'
    +	// Label by means of which the output message can be triggered
    +	label 'some_label'
    +	// input to the contract
    +	input {
    +		// the contract will be triggered by a method
    +		triggeredBy('bookReturnedTriggered()')
    +	}
    +	// output message of the contract
    +	outputMessage {
    +		// destination to which the output message will be sent
    +		sentTo('output')
    +		// the body of the output message
    +		body('''{ "bookName" : "foo" }''')
    +		// the headers of the output message
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    +

    YAML.  +

    # Human readable description
    +description: Some description
    +# Label by means of which the output message can be triggered
    +label: some_label
    +input:
    +  # the contract will be triggered by a method
    +  triggeredBy: bookReturnedTriggered()
    +# output message of the contract
    +outputMessage:
    +  # destination to which the output message will be sent
    +  sentTo: output
    +  # the body of the output message
    +  body:
    +    bookName: foo
    +  # the headers of the output message
    +  headers:
    +    BOOK-NAME: foo

    +

    In the previous example case, the output message is sent to output if a method called +bookReturnedTriggered is executed. On the message publisher’s side, we generate a +test that calls that method to trigger the message. On the consumer side, you can use +the some_label to trigger the message.

    94.11.2 Output Triggered by a Message

    The output message can be triggered by receiving a message, as shown in the following +example:

    Groovy DSL.  +

    def dsl = Contract.make {
    +	description 'Some Description'
    +	label 'some_label'
    +	// input is a message
    +	input {
    +		// the message was received from this destination
    +		messageFrom('input')
    +		// has the following body
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		// and the following headers
    +		messageHeaders {
    +			header('sample', 'header')
    +		}
    +	}
    +	outputMessage {
    +		sentTo('output')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    +

    YAML.  +

    # Human readable description
    +description: Some description
    +# Label by means of which the output message can be triggered
    +label: some_label
    +# input is a message
    +input:
    +  messageFrom: input
    +  # has the following body
    +  messageBody:
    +    bookName: 'foo'
    +  # and the following headers
    +  messageHeaders:
    +    sample: 'header'
    +# output message of the contract
    +outputMessage:
    +  # destination to which the output message will be sent
    +  sentTo: output
    +  # the body of the output message
    +  body:
    +    bookName: foo
    +  # the headers of the output message
    +  headers:
    +    BOOK-NAME: foo

    +

    In the preceding example, the output message is sent to output if a proper message is +received on the input destination. On the message publisher’s side, the engine +generates a test that sends the input message to the defined destination. On the +consumer side, you can either send a message to the input destination or use a label +(some_label in the example) to trigger the message.

    94.11.3 Consumer/Producer

    [Important]Important

    This section is valid only for Groovy DSL.

    In HTTP, you have a notion of client/stub and `server/test notation. You can also +use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also +provides the consumer and producer methods, as presented in the following example +(note that you can use either $ or value methods to provide consumer and producer +parts):

    					Contract.make {
    +						label 'some_label'
    +						input {
    +							messageFrom value(consumer('jms:output'), producer('jms:input'))
    +							messageBody([
    +									bookName: 'foo'
    +							])
    +							messageHeaders {
    +								header('sample', 'header')
    +							}
    +						}
    +						outputMessage {
    +							sentTo $(consumer('jms:input'), producer('jms:output'))
    +							body([
    +									bookName: 'foo'
    +							])
    +						}
    +					}

    94.11.4 Common

    In the input or outputMessage section you can call assertThat with the name +of a method (e.g. assertThatMessageIsOnTheQueue()) that you have defined in the +base class or in a static import. Spring Cloud Contract will execute that method +in the generated test.

    94.12 Multiple Contracts in One File

    You can define multiple contracts in one file. Such a contract might resemble the +following example:

    Groovy DSL.  +

    import org.springframework.cloud.contract.spec.Contract
    +
    +[
    +	Contract.make {
    +		name("should post a user")
    +		request {
    +			method 'POST'
    +			url('/users/1')
    +		}
    +		response {
    +			status OK()
    +		}
    +	},
    +	Contract.make {
    +		request {
    +			method 'POST'
    +			url('/users/2')
    +		}
    +		response {
    +			status OK()
    +		}
    +	}
    +]

    +

    YAML.  +

    ---
    +name: should post a user
    +request:
    +  method: POST
    +  url: /users/1
    +response:
    +  status: 200
    +---
    +request:
    +  method: POST
    +  url: /users/2
    +response:
    +  status: 200
    +---
    +request:
    +  method: POST
    +  url: /users/3
    +response:
    +  status: 200

    +

    In the preceding example, one contract has the name field and the other does not. This +leads to generation of two tests that look more or less like this:

    package org.springframework.cloud.contract.verifier.tests.com.hello;
    +
    +import com.example.TestBase;
    +import com.jayway.jsonpath.DocumentContext;
    +import com.jayway.jsonpath.JsonPath;
    +import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
    +import com.jayway.restassured.response.ResponseOptions;
    +import org.junit.Test;
    +
    +import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;
    +import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +public class V1Test extends TestBase {
    +
    +	@Test
    +	public void validate_should_post_a_user() throws Exception {
    +		// given:
    +			MockMvcRequestSpecification request = given();
    +
    +		// when:
    +			ResponseOptions response = given().spec(request)
    +					.post("/users/1");
    +
    +		// then:
    +			assertThat(response.statusCode()).isEqualTo(200);
    +	}
    +
    +	@Test
    +	public void validate_withList_1() throws Exception {
    +		// given:
    +			MockMvcRequestSpecification request = given();
    +
    +		// when:
    +			ResponseOptions response = given().spec(request)
    +					.post("/users/2");
    +
    +		// then:
    +			assertThat(response.statusCode()).isEqualTo(200);
    +	}
    +
    +}

    Notice that, for the contract that has the name field, the generated test method is named +validate_should_post_a_user. For the one that does not have the name, it is called +validate_withList_1. It corresponds to the name of the file WithList.groovy and the +index of the contract in the list.

    The generated stubs is shown in the following example:

    should post a user.json
    +1_WithList.json

    As you can see, the first file got the name parameter from the contract. The second +got the name of the contract file (WithList.groovy) prefixed with the index (in this +case, the contract had an index of 1 in the list of contracts in the file).

    [Tip]Tip

    As you can see, it is much better if you name your contracts because doing so makes +your tests far more meaningful.

    94.13 Generating Spring REST Docs snippets from the contracts

    When you want to include the requests and responses of your API using Spring REST Docs, +you only need to make some minor changes to your setup if you are using MockMvc and RestAssuredMockMvc. +Simply include the following dependencies if you haven’t already.

    Maven.  +

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>
    +<dependency>
    +	<groupId>org.springframework.restdocs</groupId>
    +	<artifactId>spring-restdocs-mockmvc</artifactId>
    +	<optional>true</optional>
    +</dependency>

    +

    Gradle.  +

    testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc'

    +

    Next you need to make some changes to your base class like the following example.

    package com.example.fraud;
    +
    +import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.rules.TestName;
    +import org.junit.runner.RunWith;
    +
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.boot.test.context.SpringBootTest;
    +import org.springframework.restdocs.JUnitRestDocumentation;
    +import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    +import org.springframework.web.context.WebApplicationContext;
    +
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
    +
    +@RunWith(SpringRunner.class)
    +@SpringBootTest(classes = Application.class)
    +public abstract class FraudBaseWithWebAppSetup {
    +
    +	private static final String OUTPUT = "target/generated-snippets";
    +
    +	@Rule
    +	public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
    +
    +	@Rule
    +	public TestName testName = new TestName();
    +
    +	@Autowired
    +	private WebApplicationContext context;
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(this.context)
    +				.apply(documentationConfiguration(this.restDocumentation))
    +				.alwaysDo(document(
    +						getClass().getSimpleName() + "_" + testName.getMethodName()))
    +				.build());
    +	}
    +
    +	protected void assertThatRejectionReasonIsNull(Object rejectionReason) {
    +		assert rejectionReason == null;
    +	}
    +
    +}

    In case you are using the standalone setup, you can set up RestAssuredMockMvc like this:

    package com.example.fraud;
    +
    +import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.rules.TestName;
    +
    +import org.springframework.restdocs.JUnitRestDocumentation;
    +import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    +
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
    +
    +public abstract class FraudBaseWithStandaloneSetup {
    +
    +	private static final String OUTPUT = "target/generated-snippets";
    +
    +	@Rule
    +	public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
    +
    +	@Rule
    +	public TestName testName = new TestName();
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.standaloneSetup(MockMvcBuilders
    +				.standaloneSetup(new FraudDetectionController())
    +				.apply(documentationConfiguration(this.restDocumentation))
    +				.alwaysDo(document(
    +						getClass().getSimpleName() + "_" + testName.getMethodName())));
    +	}
    +
    +}
    [Tip]Tip

    You don’t need to specify the output directory for the generated snippets since version 1.2.0.RELEASE of Spring REST Docs.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_gateway-how-it-works.html b/Greenwich.SR5/multi/multi_gateway-how-it-works.html new file mode 100644 index 00000000..6b4ae0ff --- /dev/null +++ b/Greenwich.SR5/multi/multi_gateway-how-it-works.html @@ -0,0 +1,3 @@ + + + 112. How It Works

    112. How It Works

    Spring Cloud Gateway Diagram

    Clients make requests to Spring Cloud Gateway. If the Gateway Handler Mapping determines that a request matches a Route, it is sent to the Gateway Web Handler. This handler runs sends the request through a filter chain that is specific to the request. The reason the filters are divided by the dotted line, is that filters may execute logic before the proxy request is sent or after. All "pre" filter logic is executed, then the proxy request is made. After the proxy request is made, the "post" filter logic is executed.

    [Note]Note

    URIs defined in routes without a port will get a default port set to 80 and 443 for HTTP and HTTPS URIs respectively.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_gateway-request-predicates-factories.html b/Greenwich.SR5/multi/multi_gateway-request-predicates-factories.html new file mode 100644 index 00000000..90343796 --- /dev/null +++ b/Greenwich.SR5/multi/multi_gateway-request-predicates-factories.html @@ -0,0 +1,133 @@ + + + 114. Route Predicate Factories

    114. Route Predicate Factories

    Spring Cloud Gateway matches routes as part of the Spring WebFlux HandlerMapping infrastructure. Spring Cloud Gateway includes many built-in Route Predicate Factories. All of these predicates match on different attributes of the HTTP request. Multiple Route Predicate Factories can be combined and are combined via logical and.

    114.1 After Route Predicate Factory

    The After Route Predicate Factory takes one parameter, a datetime (which is a java ZonedDateTime). This predicate matches requests that happen after the current datetime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: after_route
    +        uri: https://example.org
    +        predicates:
    +        - After=2017-01-20T17:42:47.789-07:00[America/Denver]

    +

    This route matches any request after Jan 20, 2017 17:42 Mountain Time (Denver).

    114.2 Before Route Predicate Factory

    The Before Route Predicate Factory takes one parameter, a datetime(which is a java ZonedDateTime). This predicate matches requests that happen before the current datetime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: before_route
    +        uri: https://example.org
    +        predicates:
    +        - Before=2017-01-20T17:42:47.789-07:00[America/Denver]

    +

    This route matches any request before Jan 20, 2017 17:42 Mountain Time (Denver).

    114.3 Between Route Predicate Factory

    The Between Route Predicate Factory takes two parameters, datetime1 and datetime2 which are java ZonedDateTime objects. This predicate matches requests that happen after datetime1 and before datetime2. The datetime2 parameter must be after datetime1.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: between_route
    +        uri: https://example.org
    +        predicates:
    +        - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

    +

    This route matches any request after Jan 20, 2017 17:42 Mountain Time (Denver) and before Jan 21, 2017 17:42 Mountain Time (Denver). This could be useful for maintenance windows.

    114.4 Cookie Route Predicate Factory

    The Cookie Route Predicate Factory takes two parameters, the cookie name and a regexp (which is a Java regular expression). This predicate matches cookies that have the given name and the value matches the regular expression.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: cookie_route
    +        uri: https://example.org
    +        predicates:
    +        - Cookie=chocolate, ch.p

    +

    This route matches the request has a cookie named chocolate who’s value matches the ch.p regular expression.

    114.5 Header Route Predicate Factory

    The Header Route Predicate Factory takes two parameters, the header name and a regexp (which is a Java regular expression). This predicate matches with a header that has the given name and the value matches the regular expression.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: header_route
    +        uri: https://example.org
    +        predicates:
    +        - Header=X-Request-Id, \d+

    +

    This route matches if the request has a header named X-Request-Id whos value matches the \d+ regular expression (has a value of one or more digits).

    114.6 Host Route Predicate Factory

    The Host Route Predicate Factory takes one parameter: a list of host name patterns. The pattern is an Ant style pattern with . as the separator. This predicates matches the Host header that matches the pattern.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: host_route
    +        uri: https://example.org
    +        predicates:
    +        - Host=**.somehost.org,**.anotherhost.org

    +

    URI template variables are supported as well, such as {sub}.myhost.org.

    This route would match if the request has a Host header has the value www.somehost.org or beta.somehost.org or www.anotherhost.org.

    This predicate extracts the URI template variables (like sub defined in the example above) as a map of names and values and places it in the ServerWebExchange.getAttributes() with a key defined in ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE. Those values are then available for use by GatewayFilter Factories

    114.7 Method Route Predicate Factory

    The Method Route Predicate Factory takes a methods argument which is one or more HTTP methods to match.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: method_route
    +        uri: https://example.org
    +        predicates:
    +        - Method=GET,POST

    +

    This route would match if the request method was a GET or a POST.

    114.8 Path Route Predicate Factory

    The Path Route Predicate Factory takes two parameter: a list of Spring PathMatcher patterns and an optional flag to matchOptionalTrailingSeparator.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: host_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/{segment},/bar/{segment}

    +

    This route would match if the request path was, for example: /foo/1 or /foo/bar or /bar/baz.

    This predicate extracts the URI template variables (like segment defined in the example above) as a map of names and values and places it in the ServerWebExchange.getAttributes() with a key defined in ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE. Those values are then available for use by GatewayFilter Factories

    A utility method is available to make access to these variables easier.

    Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);
    +
    +String segment = uriVariables.get("segment");

    114.9 Query Route Predicate Factory

    The Query Route Predicate Factory takes two parameters: a required param and an optional regexp (which is a Java regular expression).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: query_route
    +        uri: https://example.org
    +        predicates:
    +        - Query=baz

    +

    This route would match if the request contained a baz query parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: query_route
    +        uri: https://example.org
    +        predicates:
    +        - Query=foo, ba.

    +

    This route would match if the request contained a foo query parameter whose value matched the ba. regexp, so bar and baz would match.

    114.10 RemoteAddr Route Predicate Factory

    The RemoteAddr Route Predicate Factory takes a list (min size 1) of sources, which are CIDR-notation (IPv4 or IPv6) strings, e.g. 192.168.0.1/16 (where 192.168.0.1 is an IP address and 16 is a subnet mask).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: remoteaddr_route
    +        uri: https://example.org
    +        predicates:
    +        - RemoteAddr=192.168.1.1/24

    +

    This route would match if the remote address of the request was, for example, 192.168.1.10.

    114.11 Weight Route Predicate Factory

    The Weight Route Predicate Factory takes two arguments group and weight (an int). The weights are calculated per group.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: weight_high
    +        uri: https://weighthigh.org
    +        predicates:
    +        - Weight=group1, 8
    +      - id: weight_low
    +        uri: https://weightlow.org
    +        predicates:
    +        - Weight=group1, 2

    +

    This route would forward ~80% of traffic to https://weighthigh.org and ~20% of traffic to https://weighlow.org

    114.11.1 Modifying the way remote addresses are resolved

    By default the RemoteAddr Route Predicate Factory uses the remote address from the incoming request. +This may not match the actual client IP address if Spring Cloud Gateway sits behind a proxy layer.

    You can customize the way that the remote address is resolved by setting a custom RemoteAddressResolver. +Spring Cloud Gateway comes with one non-default remote address resolver which is based off of the X-Forwarded-For header, XForwardedRemoteAddressResolver.

    XForwardedRemoteAddressResolver has two static constructor methods which take different approaches to security:

    XForwardedRemoteAddressResolver::trustAll returns a RemoteAddressResolver which always takes the first IP address found in the X-Forwarded-For header. +This approach is vulnerable to spoofing, as a malicious client could set an initial value for the X-Forwarded-For which would be accepted by the resolver.

    XForwardedRemoteAddressResolver::maxTrustedIndex takes an index which correlates to the number of trusted infrastructure running in front of Spring Cloud Gateway. +If Spring Cloud Gateway is, for example only accessible via HAProxy, then a value of 1 should be used. +If two hops of trusted infrastructure are required before Spring Cloud Gateway is accessible, then a value of 2 should be used.

    Given the following header value:

    X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3

    The maxTrustedIndex values below will yield the following remote addresses.

    maxTrustedIndexresult

    [Integer.MIN_VALUE,0]

    (invalid, IllegalArgumentException during initialization)

    1

    0.0.0.3

    2

    0.0.0.2

    3

    0.0.0.1

    [4, Integer.MAX_VALUE]

    0.0.0.1

    Using Java config:

    GatewayConfig.java

    RemoteAddressResolver resolver = XForwardedRemoteAddressResolver
    +    .maxTrustedIndex(1);
    +
    +...
    +
    +.route("direct-route",
    +    r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24")
    +        .uri("https://downstream1")
    +.route("proxied-route",
    +    r -> r.remoteAddr(resolver,  "10.10.1.1", "10.10.1.1/24")
    +        .uri("https://downstream2")
    +)
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_gateway-starter.html b/Greenwich.SR5/multi/multi_gateway-starter.html new file mode 100644 index 00000000..db274058 --- /dev/null +++ b/Greenwich.SR5/multi/multi_gateway-starter.html @@ -0,0 +1,11 @@ + + + 110. How to Include Spring Cloud Gateway

    110. How to Include Spring Cloud Gateway

    To include Spring Cloud Gateway in your project use the starter with group org.springframework.cloud +and artifact id spring-cloud-starter-gateway. See the Spring Cloud Project page +for details on setting up your build system with the current Spring Cloud Release Train.

    If you include the starter, but, for some reason, you do not want the gateway to be enabled, set spring.cloud.gateway.enabled=false.

    [Important]Important

    Spring Cloud Gateway is built upon Spring Boot 2.x, +Spring WebFlux, +and Project Reactor. As a consequence +many of the familiar synchronous libraries (Spring Data and Spring Security, for example) and patterns you may +not apply when using Spring Cloud Gateway. If you are unfamiliar with these projects we suggest you +begin by reading their documentation to familiarize yourself with some of the new concepts before +working with Spring Cloud Gateway.

    [Important]Important

    Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It does not work in a traditional Servlet Container or built as a WAR.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_gradle-add-gradle-plugin.html b/Greenwich.SR5/multi/multi_gradle-add-gradle-plugin.html new file mode 100644 index 00000000..6aa67c00 --- /dev/null +++ b/Greenwich.SR5/multi/multi_gradle-add-gradle-plugin.html @@ -0,0 +1,813 @@ + + + 90. Add Gradle Plugin with Dependencies

    90. Add Gradle Plugin with Dependencies

    To add a Gradle plugin with dependencies, you can use code similar to the following:

    Plugin DSL GA versions.  +

    // build.gradle
    +plugins {
    +  id "groovy"
    +  // this will work only for GA versions of Spring Cloud Contract
    +  id "org.springframework.cloud.contract" version "${GAVerifierVersion}"
    +}
    +
    +dependencyManagement {
    +	imports {
    +		mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${GAVerifierVersion}"
    +	}
    +}
    +
    +dependencies {
    +	testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    +	// example with adding Spock core and Spock Spring
    +	testCompile "org.spockframework:spock-core:${spockVersion}"
    +	testCompile "org.spockframework:spock-spring:${spockVersion}"
    +	testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +}

    +

    Plugin DSL non GA versions.  +

    // settings.gradle
    +pluginManagement {
    +	plugins {
    +		id "org.springframework.cloud.contract" version "${verifierVersion}"
    +	}
    +    repositories {
    +        // to pick from local .m2
    +        mavenLocal()
    +        // for snapshots
    +        maven { url "https://repo.spring.io/snapshot" }
    +        // for milestones
    +        maven { url "https://repo.spring.io/milestone" }
    +        // for GA versions
    +        gradlePluginPortal()
    +    }
    +}
    +
    +// build.gradle
    +plugins {
    +  id "groovy"
    +  id "org.springframework.cloud.contract"
    +}
    +
    +dependencyManagement {
    +	imports {
    +		mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifierVersion}"
    +	}
    +}
    +
    +dependencies {
    +	testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    +	// example with adding Spock core and Spock Spring
    +	testCompile "org.spockframework:spock-core:${spockVersion}"
    +	testCompile "org.spockframework:spock-spring:${spockVersion}"
    +	testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +}

    +

    Legacy Plugin Application.  +

    // build.gradle
    +buildscript {
    +	repositories {
    +		mavenCentral()
    +	}
    +	dependencies {
    +		classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
    +		classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
    +        // here you can also pass additional dependencies such as Pact or Kotlin spec e.g.:
    +        // classpath "org.springframework.cloud:spring-cloud-contract-spec-kotlin:${verifier_version}"
    +	}
    +}
    +
    +apply plugin: 'groovy'
    +apply plugin: 'spring-cloud-contract'
    +
    +dependencyManagement {
    +	imports {
    +		mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifier_version}"
    +	}
    +}
    +
    +dependencies {
    +	testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    +	// example with adding Spock core and Spock Spring
    +	testCompile "org.spockframework:spock-core:${spockVersion}"
    +	testCompile "org.spockframework:spock-spring:${spockVersion}"
    +	testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +}

    +

    90.1 Gradle and Rest Assured 2.0

    By default, Rest Assured 3.x is added to the classpath. However, to use Rest Assured 2.x +you can add it to the plugins classpath, as shown here:

    buildscript {
    +	repositories {
    +		mavenCentral()
    +	}
    +	dependencies {
    +	    classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
    +		classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
    +		classpath "com.jayway.restassured:rest-assured:2.5.0"
    +		classpath "com.jayway.restassured:spring-mock-mvc:2.5.0"
    +	}
    +}
    +
    +depenendencies {
    +    // all dependencies
    +    // you can exclude rest-assured from spring-cloud-contract-verifier
    +    testCompile "com.jayway.restassured:rest-assured:2.5.0"
    +    testCompile "com.jayway.restassured:spring-mock-mvc:2.5.0"
    +}

    That way, the plugin automatically sees that Rest Assured 2.x is present on the classpath +and modifies the imports accordingly.

    90.2 Snapshot Versions for Gradle

    Add the additional snapshot repository to your build.gradle to use snapshot versions, +which are automatically uploaded after every successful build, as shown here:

    /*
    + We need to use the [buildscript {}] section when we have to modify
    + the classpath for the plugins. If that's not the case this section
    + can be skipped.
    +
    + If you don't need to modify the classpath (e.g. add a Pact dependency),
    + then you can just set the [pluginManagement {}] section in [settings.gradle] file.
    +
    + // settings.gradle
    + pluginManagement {
    +    repositories {
    +        // for snapshots
    +        maven {url "https://repo.spring.io/snapshot"}
    +        // for milestones
    +        maven {url "https://repo.spring.io/milestone"}
    +        // for GA versions
    +        gradlePluginPortal()
    +    }
    + }
    +
    + */
    +buildscript {
    +	repositories {
    +		mavenCentral()
    +		mavenLocal()
    +		maven { url "https://repo.spring.io/snapshot" }
    +		maven { url "https://repo.spring.io/milestone" }
    +		maven { url "https://repo.spring.io/release" }
    +	}
    +}

    90.3 Add stubs

    By default, Spring Cloud Contract Verifier is looking for stubs in the +src/test/resources/contracts directory.

    The directory containing stub definitions is treated as a class name, and each stub +definition is treated as a single test. Spring Cloud Contract Verifier assumes that it +contains at least one level of directories that are to be used as the test class name. +If more than one level of nested directories is present, all except the last one is used +as the package name. For example, with following structure:

    src/test/resources/contracts/myservice/shouldCreateUser.groovy
    +src/test/resources/contracts/myservice/shouldReturnUser.groovy

    Spring Cloud Contract Verifier creates a test class named defaultBasePackage.MyService +with two methods:

    • shouldCreateUser()
    • shouldReturnUser()

    90.4 Run the Plugin

    The plugin registers itself to be invoked before a check task. If you want it to be +part of your build process, you need to do nothing more. If you just want to generate +tests, invoke the generateContractTests task.

    90.5 Default Setup

    The default Gradle Plugin setup creates the following Gradle part of the build (in +pseudocode):

    contracts {
    +    testFramework ='JUNIT'
    +    testMode = 'MockMvc'
    +    generatedTestSourcesDir = project.file("${project.buildDir}/generated-test-sources/contracts")
    +    generatedTestResourcesDir = project.file("${project.buildDir}/generated-test-resources/contracts")
    +    contractsDslDir = file("${project.rootDir}/src/test/resources/contracts")
    +    basePackageForTests = 'org.springframework.cloud.verifier.tests'
    +    stubsOutputDir = project.file("${project.buildDir}/stubs")
    +
    +    // the following properties are used when you want to provide where the JAR with contract lays
    +    contractDependency {
    +        stringNotation = ''
    +    }
    +    contractsPath = ''
    +    contractsWorkOffline = false
    +    contractRepository {
    +        cacheDownloadedContracts(true)
    +    }
    +}
    +
    +tasks.create(type: Jar, name: 'verifierStubsJar', dependsOn: 'generateClientStubs') {
    +    baseName = project.name
    +    classifier = contracts.stubsSuffix
    +    from contractVerifier.stubsOutputDir
    +}
    +
    +project.artifacts {
    +    archives task
    +}
    +
    +tasks.create(type: Copy, name: 'copyContracts') {
    +    from contracts.contractsDslDir
    +    into contracts.stubsOutputDir
    +}
    +
    +verifierStubsJar.dependsOn 'copyContracts'
    +
    +publishing {
    +    publications {
    +        stubs(MavenPublication) {
    +            artifactId project.name
    +            artifact verifierStubsJar
    +        }
    +    }
    +}

    90.6 Configure Plugin

    To change the default configuration, add a contracts snippet to your Gradle config, as +shown here:

    contracts {
    +	testMode = 'MockMvc'
    +	baseClassForTests = 'org.mycompany.tests'
    +	generatedTestSourcesDir = project.file('src/generatedContract')
    +}

    90.7 Configuration Options

    • testMode: Defines the mode for acceptance tests. By default, the mode is MockMvc, +which is based on Spring’s MockMvc. It can also be changed to WebTestClient, JaxRsClient or to +Explicit for real HTTP calls.
    • imports: Creates an array with imports that should be included in generated tests +(for example ['org.myorg.Matchers']). By default, it creates an empty array.
    • staticImports: Creates an array with static imports that should be included in +generated tests(for example ['org.myorg.Matchers.*']). By default, it creates an empty +array.
    • basePackageForTests: Specifies the base package for all generated tests. If not set, +the value is picked from baseClassForTests’s package and from `packageWithBaseClasses. +If neither of these values are set, then the value is set to +org.springframework.cloud.contract.verifier.tests.
    • baseClassForTests: Creates a base class for all generated tests. By default, if you +use Spock classes, the class is spock.lang.Specification.
    • packageWithBaseClasses: Defines a package where all the base classes reside. This +setting takes precedence over baseClassForTests.
    • baseClassMappings: Explicitly maps a contract package to a FQN of a base class. This +setting takes precedence over packageWithBaseClasses and baseClassForTests.
    • ruleClassForTests: Specifies a rule that should be added to the generated test +classes.
    • ignoredFiles: Uses an Antmatcher to allow defining stub files for which processing +should be skipped. By default, it is an empty array.
    • contractsDslDir: Specifies the directory containing contracts written using the +GroovyDSL. By default, its value is $rootDir/src/test/resources/contracts.
    • generatedTestSourcesDir: Specifies the test source directory where tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-sources/contracts.
    • generatedTestResourcesDir: Specifies the test resource directory where resources used by the tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-resources/contracts.
    • stubsOutputDir: Specifies the directory where the generated WireMock stubs from +the Groovy DSL should be placed.
    • testFramework: Specifies the target test framework to be used. Currently, Spock, JUnit 4 (TestFramework.JUNIT) and +JUnit 5 are supported with JUnit 4 being the default framework.
    • contractsProperties: a map containing properties to be passed to Spring Cloud Contract +components. Those properties might be used by e.g. inbuilt or custom Stub Downloaders.

    The following properties are used when you want to specify the location of the JAR +containing the contracts:

    • contractDependency: Specifies the Dependency that provides +groupid:artifactid:version:classifier coordinates. You can use the contractDependency +closure to set it up.
    • contractsPath: Specifies the path to the jar. If contract dependencies are +downloaded, the path defaults to groupid/artifactid where groupid is slash +separated. Otherwise, it scans contracts under the provided directory.
    • contractsMode: Specifies the mode of downloading contracts (whether the +JAR is available offline, remotely etc.)
    • deleteStubsAfterTest: If set to false will not remove any downloaded +contracts from temporary directories

    Below you can find a list of experimental features you can turn on via the plugin:

    • convertToYaml: converts all DSLs to the declarative, YAML format. This can be extremely useful when you’re using external libraries in your Groovy DSLs. By turning this feature on (by setting it to true) you will not need to add the library dependency on the consumer side.
    • assertJsonSize: You can check the size of JSON arrays in the generated tests. This feature is disabled by default.

    90.8 Single Base Class for All Tests

    When using Spring Cloud Contract Verifier in default MockMvc, you need to create a base +specification for all generated acceptance tests. In this class, you need to point to an +endpoint, which should be verified.

    abstract class BaseMockMvcSpec extends Specification {
    +
    +	def setup() {
    +		RestAssuredMockMvc.standaloneSetup(new PairIdController())
    +	}
    +
    +	void isProperCorrelationId(Integer correlationId) {
    +		assert correlationId == 123456
    +	}
    +
    +	void isEmpty(String value) {
    +		assert value == null
    +	}
    +
    +}

    If you use Explicit mode, you can use a base class to initialize the whole tested app +as you might see in regular integration tests. If you use the JAXRSCLIENT mode, this +base class should also contain a protected WebTarget webTarget field. Right now, the +only option to test the JAX-RS API is to start a web server.

    90.9 Different Base Classes for Contracts

    If your base classes differ between contracts, you can tell the Spring Cloud Contract +plugin which class should get extended by the autogenerated tests. You have two options:

    • Follow a convention by providing the packageWithBaseClasses
    • Provide explicit mapping via baseClassMappings

    By Convention

    The convention is such that if you have a contract under (for example) +src/test/resources/contract/foo/bar/baz/ and set the value of the +packageWithBaseClasses property to com.example.base, then Spring Cloud Contract +Verifier assumes that there is a BarBazBase class under the com.example.base package. +In other words, the system takes the last two parts of the package, if they exist, and +forms a class with a Base suffix. This rule takes precedence over baseClassForTests. +Here is an example of how it works in the contracts closure:

    packageWithBaseClasses = 'com.example.base'

    By Mapping

    You can manually map a regular expression of the contract’s package to fully qualified +name of the base class for the matched contract. You have to provide a list called +baseClassMappings that consists baseClassMapping objects that takes a +contractPackageRegex to baseClassFQN mapping. Consider the following example:

    baseClassForTests = "com.example.FooBase"
    +baseClassMappings {
    +	baseClassMapping('.*/com/.*', 'com.example.ComBase')
    +	baseClassMapping('.*/bar/.*': 'com.example.BarBase')
    +}

    Let’s assume that you have contracts under + - src/test/resources/contract/com/ + - src/test/resources/contract/foo/

    By providing the baseClassForTests, we have a fallback in case mapping did not succeed. +(You could also provide the packageWithBaseClasses as a fallback.) That way, the tests +generated from src/test/resources/contract/com/ contracts extend the +com.example.ComBase, whereas the rest of the tests extend com.example.FooBase.

    90.10 Invoking Generated Tests

    To ensure that the provider side is compliant with defined contracts, you need to invoke:

    ./gradlew generateContractTests test

    90.11 Pushing stubs to SCM

    If you’re using the SCM repository to keep the contracts and +stubs, you might want to automate the step of pushing stubs to +the repository. To do that, it’s enough to call the pushStubsToScm +task. Example:

    $ ./gradlew pushStubsToScm

    Under Section 96.6, “Using the SCM Stub Downloader” you can find all possible +configuration options that you can pass either via +the contractsProperties field e.g. contracts { contractsProperties = [foo:"bar"] }, +via contractsProperties method e.g. contracts { contractsProperties([foo:"bar"]) }, +a system property or an environment variable.

    90.12 Spring Cloud Contract Verifier on the Consumer Side

    In a consuming service, you need to configure the Spring Cloud Contract Verifier plugin +in exactly the same way as in case of provider. If you do not want to use Stub Runner +then you need to copy contracts stored in src/test/resources/contracts and generate +WireMock JSON stubs using:

    ./gradlew generateClientStubs
    [Note]Note

    The stubsOutputDir option has to be set for stub generation to work.

    When present, JSON stubs can be used in automated tests of consuming a service.

    @ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application)
    +class LoanApplicationServiceSpec extends Specification {
    +
    + @ClassRule
    + @Shared
    + WireMockClassRule wireMockRule == new WireMockClassRule()
    +
    + @Autowired
    + LoanApplicationService sut
    +
    + def 'should successfully apply for loan'() {
    +   given:
    + 	LoanApplication application =
    +			new LoanApplication(client: new Client(clientPesel: '12345678901'), amount: 123.123)
    +   when:
    +	LoanApplicationResult loanApplication == sut.loanApplication(application)
    +   then:
    +	loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLIED
    +	loanApplication.rejectionReason == null
    + }
    +}

    LoanApplication makes a call to FraudDetection service. This request is handled by a +WireMock server configured with stubs generated by Spring Cloud Contract Verifier.

    90.13 Maven Project

    To learn how to set up the Maven project for Spring Cloud Contract Verifier, read the +following sections:

    90.13.1 Add maven plugin

    Add the Spring Cloud Contract BOM in a fashion similar to this:

    <dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>${spring-cloud-release.version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    Next, add the Spring Cloud Contract Verifier Maven plugin:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +		<convertToYaml>true</convertToYaml>
    +	</configuration>
    +</plugin>

    You can read more in the +Spring +Cloud Contract Maven Plugin Documentation (example for 2.0.0.RELEASE version).

    90.13.2 Maven and Rest Assured 2.0

    By default, Rest Assured 3.x is added to the classpath. However, you can use Rest +Assured 2.x by adding it to the plugins classpath, as shown here:

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <packageWithBaseClasses>com.example</packageWithBaseClasses>
    +    </configuration>
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-contract-verifier</artifactId>
    +            <version>${spring-cloud-contract.version}</version>
    +        </dependency>
    +        <dependency>
    +           <groupId>com.jayway.restassured</groupId>
    +           <artifactId>rest-assured</artifactId>
    +           <version>2.5.0</version>
    +           <scope>compile</scope>
    +        </dependency>
    +        <dependency>
    +           <groupId>com.jayway.restassured</groupId>
    +           <artifactId>spring-mock-mvc</artifactId>
    +           <version>2.5.0</version>
    +           <scope>compile</scope>
    +        </dependency>
    +    </dependencies>
    +</plugin>
    +
    +<dependencies>
    +    <!-- all dependencies -->
    +    <!-- you can exclude rest-assured from spring-cloud-contract-verifier -->
    +    <dependency>
    +       <groupId>com.jayway.restassured</groupId>
    +       <artifactId>rest-assured</artifactId>
    +       <version>2.5.0</version>
    +       <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +       <groupId>com.jayway.restassured</groupId>
    +       <artifactId>spring-mock-mvc</artifactId>
    +       <version>2.5.0</version>
    +       <scope>test</scope>
    +    </dependency>
    +</dependencies>

    That way, the plugin automatically sees that Rest Assured 3.x is present on the classpath +and modifies the imports accordingly.

    90.13.3 Snapshot versions for Maven

    For Snapshot and Milestone versions, you have to add the following section to your +pom.xml, as shown here:

    <repositories>
    +	<repository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +</repositories>
    +<pluginRepositories>
    +	<pluginRepository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +</pluginRepositories>

    90.13.4 Add stubs

    By default, Spring Cloud Contract Verifier is looking for stubs in the +src/test/resources/contracts directory. The directory containing stub definitions is +treated as a class name, and each stub definition is treated as a single test. We assume +that it contains at least one directory to be used as test class name. If there is more +than one level of nested directories, all except the last one is used as package name. +For example, with following structure:

    src/test/resources/contracts/myservice/shouldCreateUser.groovy
    +src/test/resources/contracts/myservice/shouldReturnUser.groovy

    Spring Cloud Contract Verifier creates a test class named defaultBasePackage.MyService +with two methods

    • shouldCreateUser()
    • shouldReturnUser()

    90.13.5 Run plugin

    The plugin goal generateTests is assigned to be invoked in the phase called +generate-test-sources. If you want it to be part of your build process, you need not do +anything. If you just want to generate tests, invoke the generateTests goal.

    90.13.6 Configure plugin

    To change the default configuration, just add a configuration section to the plugin +definition or the execution definition, as shown here:

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <executions>
    +        <execution>
    +            <goals>
    +                <goal>convert</goal>
    +                <goal>generateStubs</goal>
    +                <goal>generateTests</goal>
    +            </goals>
    +        </execution>
    +    </executions>
    +    <configuration>
    +        <basePackageForTests>org.springframework.cloud.verifier.twitter.place</basePackageForTests>
    +        <baseClassForTests>org.springframework.cloud.verifier.twitter.place.BaseMockMvcSpec</baseClassForTests>
    +    </configuration>
    +</plugin>

    90.13.7 Configuration Options

    • testMode: Defines the mode for acceptance tests. By default, the mode is MockMvc, +which is based on Spring’s MockMvc. It can also be changed to WebTestClient, JaxRsClient or to +Explicit for real HTTP calls.
    • basePackageForTests: Specifies the base package for all generated tests. If not set, +the value is picked from baseClassForTests’s package and from `packageWithBaseClasses. +If neither of these values are set, then the value is set to +org.springframework.cloud.contract.verifier.tests.
    • ruleClassForTests: Specifies a rule that should be added to the generated test +classes.
    • baseClassForTests: Creates a base class for all generated tests. By default, if you +use Spock classes, the class is spock.lang.Specification.
    • contractsDirectory: Specifies a directory containing contracts written with the +GroovyDSL. The default directory is /src/test/resources/contracts.
    • generatedTestSourcesDir: Specifies the test source directory where tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-sources/contracts.
    • generatedTestResourcesDir: Specifies the test resource directory where resources used by the tests generated
    • testFramework: Specifies the target test framework to be used. Currently, Spock, JUnit 4 (TestFramework.JUNIT) and +JUnit 5 are supported with JUnit 4 being the default framework.
    • packageWithBaseClasses: Defines a package where all the base classes reside. This +setting takes precedence over baseClassForTests. The convention is such that, if you +have a contract under (for example) src/test/resources/contract/foo/bar/baz/ and set +the value of the packageWithBaseClasses property to com.example.base, then Spring +Cloud Contract Verifier assumes that there is a BarBazBase class under the +com.example.base package. In other words, the system takes the last two parts of the +package, if they exist, and forms a class with a Base suffix.
    • baseClassMappings: Specifies a list of base class mappings that provide +contractPackageRegex, which is checked against the package where the contract is +located, and baseClassFQN, which maps to the fully qualified name of the base class for +the matched contract. For example, if you have a contract under +src/test/resources/contract/foo/bar/baz/ and map the property +.* → com.example.base.BaseClass, then the test class generated from these contracts +extends com.example.base.BaseClass. This setting takes precedence over +packageWithBaseClasses and baseClassForTests.
    • contractsProperties: a map containing properties to be passed to Spring Cloud Contract +components. Those properties might be used by e.g. inbuilt or custom Stub Downloaders.

    If you want to download your contract definitions from a Maven repository, you can use +the following options:

    • contractDependency: The contract dependency that contains all the packaged contracts.
    • contractsPath: The path to the concrete contracts in the JAR with packaged contracts. +Defaults to groupid/artifactid where gropuid is slash separated.
    • contractsMode: Picks the mode in which stubs will be found and registered
    • deleteStubsAfterTest: If set to false will not remove any downloaded +contracts from temporary directories
    • contractsRepositoryUrl: URL to a repo with the artifacts that have contracts. If it is not provided, +use the current Maven ones.
    • contractsRepositoryUsername: The user name to be used to connect to the repo with contracts.
    • contractsRepositoryPassword: The password to be used to connect to the repo with contracts.
    • contractsRepositoryProxyHost: The proxy host to be used to connect to the repo with contracts.
    • contractsRepositoryProxyPort: The proxy port to be used to connect to the repo with contracts.

    We cache only non-snapshot, explicitly provided versions (for example ++ or 1.0.0.BUILD-SNAPSHOT won’t get cached). By default, this feature is turned on.

    Below you can find a list of experimental features you can turn on via the plugin:

    • convertToYaml: converts all DSLs to the declarative, YAML format. This can be extremely useful when you’re using external libraries in your Groovy DSLs. By turning this feature on (by setting it to true) you will not need to add the library dependency on the consumer side.
    • assertJsonSize: You can check the size of JSON arrays in the generated tests. This feature is disabled by default.

    90.13.8 Single Base Class for All Tests

    When using Spring Cloud Contract Verifier in default MockMvc, you need to create a base +specification for all generated acceptance tests. In this class, you need to point to an +endpoint, which should be verified.

    package org.mycompany.tests
    +
    +import org.mycompany.ExampleSpringController
    +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc
    +import spock.lang.Specification
    +
    +class MvcSpec extends Specification {
    +  def setup() {
    +   RestAssuredMockMvc.standaloneSetup(new ExampleSpringController())
    +  }
    +}

    You can also setup the whole context if necessary.

    import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +import org.junit.runner.RunWith;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.boot.test.context.SpringBootTest;
    +import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.web.context.WebApplicationContext;
    +
    +@RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SomeConfig.class, properties="some=property")
    +public abstract class BaseTestClass {
    +
    +	@Autowired
    +	WebApplicationContext context;
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.webAppContextSetup(this.context);
    +	}
    +}

    If you use EXPLICIT mode, you can use a base class to initialize the whole tested app +similarly, as you might find in regular integration tests.

    import io.restassured.RestAssured;
    +import org.junit.Before;
    +import org.junit.runner.RunWith;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.boot.test.context.SpringBootTest;
    +import org.springframework.boot.web.server.LocalServerPort
    +import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.web.context.WebApplicationContext;
    +
    +@RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SomeConfig.class, properties="some=property")
    +public abstract class BaseTestClass {
    +
    +	@LocalServerPort
    +	int port;
    +
    +	@Before
    +	public void setup() {
    +		RestAssured.baseURI = "http://localhost:" + this.port;
    +	}
    +}

    If you use the JAXRSCLIENT mode, this base class should also contain a protected WebTarget webTarget field. Right +now, the only option to test the JAX-RS API is to start a web server.

    90.13.9 Different base classes for contracts

    If your base classes differ between contracts, you can tell the Spring Cloud Contract +plugin which class should get extended by the autogenerated tests. You have two options:

    • Follow a convention by providing the packageWithBaseClasses
    • provide explicit mapping via baseClassMappings

    By Convention

    The convention is such that if you have a contract under (for example) +src/test/resources/contract/foo/bar/baz/ and set the value of the +packageWithBaseClasses property to com.example.base, then Spring Cloud Contract +Verifier assumes that there is a BarBazBase class under the com.example.base package. +In other words, the system takes the last two parts of the package, if they exist, and +forms a class with a Base suffix. This rule takes precedence over baseClassForTests. +Here is an example of how it works in the contracts closure:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<configuration>
    +		<packageWithBaseClasses>hello</packageWithBaseClasses>
    +	</configuration>
    +</plugin>

    By Mapping

    You can manually map a regular expression of the contract’s package to fully qualified +name of the base class for the matched contract. You have to provide a list called +baseClassMappings that consists baseClassMapping objects that takes a +contractPackageRegex to baseClassFQN mapping. Consider the following example:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<configuration>
    +		<baseClassForTests>com.example.FooBase</baseClassForTests>
    +		<baseClassMappings>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*com.*</contractPackageRegex>
    +				<baseClassFQN>com.example.TestBase</baseClassFQN>
    +			</baseClassMapping>
    +		</baseClassMappings>
    +	</configuration>
    +</plugin>

    Assume that you have contracts under these two locations: +* src/test/resources/contract/com/ +* src/test/resources/contract/foo/

    By providing the baseClassForTests, we have a fallback in case mapping did not succeed. +(You can also provide the packageWithBaseClasses as a fallback.) That way, the tests +generated from src/test/resources/contract/com/ contracts extend the +com.example.ComBase, whereas the rest of the tests extend com.example.FooBase.

    90.13.10 Invoking generated tests

    The Spring Cloud Contract Maven Plugin generates verification code in a directory called +/generated-test-sources/contractVerifier and attaches this directory to testCompile +goal.

    For Groovy Spock code, use the following:

    <plugin>
    +	<groupId>org.codehaus.gmavenplus</groupId>
    +	<artifactId>gmavenplus-plugin</artifactId>
    +	<version>1.5</version>
    +	<executions>
    +		<execution>
    +			<goals>
    +				<goal>testCompile</goal>
    +			</goals>
    +		</execution>
    +	</executions>
    +	<configuration>
    +		<testSources>
    +			<testSource>
    +				<directory>${project.basedir}/src/test/groovy</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +				</includes>
    +			</testSource>
    +			<testSource>
    +				<directory>${project.build.directory}/generated-test-sources/contractVerifier</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +				</includes>
    +			</testSource>
    +		</testSources>
    +	</configuration>
    +</plugin>

    To ensure that provider side is compliant with defined contracts, you need to invoke +mvn generateTest test.

    90.13.11 Pushing stubs to SCM

    If you’re using the SCM repository to keep the contracts and +stubs, you might want to automate the step of pushing stubs to +the repository. To do that, it’s enough to add the pushStubsToScm +goal. Example:

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <!-- Base class mappings etc. -->
    +
    +        <!-- We want to pick contracts from a Git repository -->
    +        <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl>
    +
    +        <!-- We reuse the contract dependency section to set up the path
    +        to the folder that contains the contract definitions. In our case the
    +        path will be /groupId/artifactId/version/contracts -->
    +        <contractDependency>
    +            <groupId>${project.groupId}</groupId>
    +            <artifactId>${project.artifactId}</artifactId>
    +            <version>${project.version}</version>
    +        </contractDependency>
    +
    +        <!-- The contracts mode can't be classpath -->
    +        <contractsMode>REMOTE</contractsMode>
    +    </configuration>
    +    <executions>
    +        <execution>
    +            <phase>package</phase>
    +            <goals>
    +                <!-- By default we will not push the stubs back to SCM,
    +                you have to explicitly add it as a goal -->
    +                <goal>pushStubsToScm</goal>
    +            </goals>
    +        </execution>
    +    </executions>
    +</plugin>

    Under Section 96.6, “Using the SCM Stub Downloader” you can find all possible +configuration options that you can pass either via +the <configuration><contractProperties> map, a system property +or an environment variable.

    90.13.12 Maven Plugin and STS

    If you see the following exception while using STS:

    STS Exception

    When you click on the error marker you should see something like this:

     plugin:1.1.0.M1:convert:default-convert:process-test-resources) org.apache.maven.plugin.PluginExecutionException: Execution default-convert of goal org.springframework.cloud:spring-
    + cloud-contract-maven-plugin:1.1.0.M1:convert failed. at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:145) at
    + org.eclipse.m2e.core.internal.embedder.MavenImpl.execute(MavenImpl.java:331) at org.eclipse.m2e.core.internal.embedder.MavenImpl$11.call(MavenImpl.java:1362) at
    +...
    + org.eclipse.core.internal.jobs.Worker.run(Worker.java:55) Caused by: java.lang.NullPointerException at
    + org.eclipse.m2e.core.internal.builder.plexusbuildapi.EclipseIncrementalBuildContext.hasDelta(EclipseIncrementalBuildContext.java:53) at
    + org.sonatype.plexus.build.incremental.ThreadBuildContext.hasDelta(ThreadBuildContext.java:59) at

    In order to fix this issue, provide the following section in your pom.xml:

    <build>
    +    <pluginManagement>
    +        <plugins>
    +            <!--This plugin's configuration is used to store Eclipse m2e settings
    +                only. It has no influence on the Maven build itself. -->
    +            <plugin>
    +                <groupId>org.eclipse.m2e</groupId>
    +                <artifactId>lifecycle-mapping</artifactId>
    +                <version>1.0.0</version>
    +                <configuration>
    +                    <lifecycleMappingMetadata>
    +                        <pluginExecutions>
    +                             <pluginExecution>
    +                                <pluginExecutionFilter>
    +                                    <groupId>org.springframework.cloud</groupId>
    +                                    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +                                    <versionRange>[1.0,)</versionRange>
    +                                    <goals>
    +                                        <goal>convert</goal>
    +                                    </goals>
    +                                </pluginExecutionFilter>
    +                                <action>
    +                                    <execute />
    +                                </action>
    +                             </pluginExecution>
    +                        </pluginExecutions>
    +                    </lifecycleMappingMetadata>
    +                </configuration>
    +            </plugin>
    +        </plugins>
    +    </pluginManagement>
    +</build>

    90.13.13 Maven Plugin with Spock Tests

    You can select the Spock Framework for creating and executing the auto-generated contract +verification tests with both Maven and Gradle plugin. However, whereas with Gradle its really straightforward, +in Maven you will require some additional setup in order to make the tests compile and execute properly.

    First of all, you will have to use a plugin, such as GMavenPlus plugin, +to add Groovy to your project. In GMavenPlus plugin, you will need to explicitly set test sources, including both the +path where your base test classes are defined and the path were the generated contract tests are added. +Please refer to the example below:

    <plugin>
    +	<groupId>org.codehaus.gmavenplus</groupId>
    +	<artifactId>gmavenplus-plugin</artifactId>
    +	<version>1.6.1</version>
    +	<executions>
    +		<execution>
    +			<goals>
    +				<goal>compileTests</goal>
    +				<goal>addTestSources</goal>
    +			</goals>
    +		</execution>
    +	</executions>
    +	<configuration>
    +		<testSources>
    +			<testSource>
    +				<directory>${project.basedir}/src/test/groovy</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +				</includes>
    +			</testSource>
    +			<testSource>
    +				<directory>
    +					${project.basedir}/target/generated-test-sources/contracts/com/example/beer
    +				</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +					<include>**/*.gvy</include>
    +				</includes>
    +			</testSource>
    +		</testSources>
    +	</configuration>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.codehaus.groovy</groupId>
    +			<artifactId>groovy-all</artifactId>
    +			<version>2.4.15</version>
    +			<scope>runtime</scope>
    +			<type>pom</type>
    +		</dependency>
    +	</dependencies>

    If you uphold to the Spock convention of ending the test class names with Spec, you will also need to adjust your Maven +Surefire plugin setup, like in the following example:

    90.14 Stubs and Transitive Dependencies

    The Maven and Gradle plugin that add the tasks that create the stubs jar for you. One +problem that arises is that, when reusing the stubs, you can mistakenly import all of +that stub’s dependencies. When building a Maven artifact, even though you have a couple +of different jars, all of them share one pom:

    ├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar
    +├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar.sha1
    +├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar
    +├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar.sha1
    +├── github-webhook-0.0.1.BUILD-SNAPSHOT.jar
    +├── github-webhook-0.0.1.BUILD-SNAPSHOT.pom
    +├── github-webhook-0.0.1.BUILD-SNAPSHOT-stubs.jar
    +├── ...
    +└── ...

    There are three possibilities of working with those dependencies so as not to have any +issues with transitive dependencies:

    • Mark all application dependencies as optional
    • Create a separate artifactid for the stubs
    • Exclude dependencies on the consumer side

    Mark all application dependencies as optional

    If, in the github-webhook application, you mark all of your dependencies as optional, +when you include the github-webhook stubs in another application (or when that +dependency gets downloaded by Stub Runner) then, since all of the dependencies are +optional, they will not get downloaded.

    Create a separate artifactid for the stubs

    If you create a separate artifactid, then you can set it up in whatever way you wish. +For example, you might decide to have no dependencies at all.

    Exclude dependencies on the consumer side

    As a consumer, if you add the stub dependency to your classpath, you can explicitly +exclude the unwanted dependencies.

    90.15 Scenarios

    You can handle scenarios with Spring Cloud Contract Verifier. All you need to do is to +stick to the proper naming convention while creating your contracts. The convention +requires including an order number followed by an underscore. This will work regardles + of whether you’re working with YAML or Groovy. Example:

    my_contracts_dir\
    +  scenario1\
    +    1_login.groovy
    +    2_showCart.groovy
    +    3_logout.groovy

    Such a tree causes Spring Cloud Contract Verifier to generate WireMock’s scenario with a +name of scenario1 and the three following steps:

    1. login marked as Started pointing to…​
    2. showCart marked as Step1 pointing to…​
    3. logout marked as Step2 which will close the scenario.

    More details about WireMock scenarios can be found at +https://wiremock.org/docs/stateful-behaviour/

    Spring Cloud Contract Verifier also generates tests with a guaranteed order of execution.

    90.16 Docker Project

    We’re publishing a springcloud/spring-cloud-contract Docker image +that contains a project that will generate tests and execute them in EXPLICIT mode +against a running application.

    [Tip]Tip

    The EXPLICIT mode means that the tests generated from contracts will send +real requests and not the mocked ones.

    90.16.1 Short intro to Maven, JARs and Binary storage

    Since the Docker image can be used by non JVM projects, it’s good to +explain the basic terms behind Spring Cloud Contract packaging defaults.

    Part of the following definitions were taken from the Maven Glossary

    • Project: Maven thinks in terms of projects. Everything that you +will build are projects. Those projects follow a well defined +“Project Object Model”. Projects can depend on other projects, +in which case the latter are called “dependencies”. A project may +consistent of several subprojects, however these subprojects are still +treated equally as projects.
    • Artifact: An artifact is something that is either produced or used +by a project. Examples of artifacts produced by Maven for a project +include: JARs, source and binary distributions. Each artifact +is uniquely identified by a group id and an artifact ID which is +unique within a group.
    • JAR: JAR stands for Java ARchive. It’s a format based on +the ZIP file format. Spring Cloud Contract packages the contracts and generated +stubs in a JAR file.
    • GroupId: A group ID is a universally unique identifier for a project. +While this is often just the project name (eg. commons-collections), +it is helpful to use a fully-qualified package name to distinguish it +from other projects with a similar name (eg. org.apache.maven). +Typically, when published to the Artifact Manager, the GroupId will get +slash separated and form part of the URL. E.g. for group id com.example +and artifact id application would be /com/example/application/.
    • Classifier: The Maven dependency notation looks as follows: +groupId:artifactId:version:classifier. The classifier is additional suffix +passed to the dependency. E.g. stubs, sources. The same dependency +e.g. com.example:application can produce multiple artifacts that +differ from each other with the classifier.
    • Artifact manager: When you generate binaries / sources / packages, you would +like them to be available for others to download / reference or reuse. In case +of the JVM world those artifacts would be JARs, for Ruby these are gems +and for Docker those would be Docker images. You can store those artifacts +in a manager. Examples of such managers can be Artifactory +or Nexus.

    90.16.2 How it works

    The image searches for contracts under the /contracts folder. +The output from running the tests will be available under +/spring-cloud-contract/build folder (it’s useful for debugging +purposes).

    It’s enough for you to mount your contracts, pass the environment variables + and the image will:

    • generate the contract tests
    • execute the tests against the provided URL
    • generate the WireMock stubs
    • (optional - turned on by default) publish the stubs to a Artifact Manager

    Environment Variables

    The Docker image requires some environment variables to point to +your running application, to the Artifact manager instance etc.

    • PROJECT_GROUP - your project’s group id. Defaults to com.example
    • PROJECT_VERSION - your project’s version. Defaults to 0.0.1-SNAPSHOT
    • PROJECT_NAME - artifact id. Defaults to example
    • REPO_WITH_BINARIES_URL - URL of your Artifact Manager. Defaults to http://localhost:8081/artifactory/libs-release-local +which is the default URL of Artifactory running locally
    • REPO_WITH_BINARIES_USERNAME - (optional) username when the Artifact Manager is secured
    • REPO_WITH_BINARIES_PASSWORD - (optional) password when the Artifact Manager is secured
    • PUBLISH_ARTIFACTS - if set to true then will publish artifact to binary storage. Defaults to true.

    These environment variables are used when contracts lay in an external repository. To enable +this feature you must set the EXTERNAL_CONTRACTS_ARTIFACT_ID environment variable.

    • EXTERNAL_CONTRACTS_GROUP_ID - group id of the project with contracts. Defaults to com.example
    • EXTERNAL_CONTRACTS_ARTIFACT_ID- artifact id of the project with contracts.
    • EXTERNAL_CONTRACTS_CLASSIFIER- classifier of the project with contracts. Empty by default
    • EXTERNAL_CONTRACTS_VERSION - version of the project with contracts. Defaults to +, equivalent to picking the latest
    • EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL - URL of your Artifact Manager. Defaults to value of REPO_WITH_BINARIES_URL env var. +If that’s not set, defaults to http://localhost:8081/artifactory/libs-release-local +which is the default URL of Artifactory running locally
    • EXTERNAL_CONTRACTS_PATH - path to contracts for the given project, inside the project with contracts. +Defaults to slash separated EXTERNAL_CONTRACTS_GROUP_ID concatenated with / and EXTERNAL_CONTRACTS_ARTIFACT_ID. E.g. +for group id foo.bar and artifact id baz, would result in foo/bar/baz contracts path.
    • EXTERNAL_CONTRACTS_WORK_OFFLINE - if set to true then will retrieve artifact with contracts +from the container’s .m2. Mount your local .m2 as a volume available at the container’s /root/.m2 path. +You must not set both EXTERNAL_CONTRACTS_WORK_OFFLINE and EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL.

    These environment variables are used when tests are executed:

    • APPLICATION_BASE_URL - url against which tests should be executed. +Remember that it has to be accessible from the Docker container (e.g. localhost +will not work)
    • APPLICATION_USERNAME - (optional) username for basic authentication to your application
    • APPLICATION_PASSWORD - (optional) password for basic authentication to your application

    90.16.3 Example of usage

    Let’s take a look at a simple MVC application

    $ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
    +$ cd bookstore

    The contracts are available under /contracts folder.

    90.16.4 Server side (nodejs)

    Since we want to run tests, we could just execute:

    $ npm test

    however, for learning purposes, let’s split it into pieces:

    # Stop docker infra (nodejs, artifactory)
    +$ ./stop_infra.sh
    +# Start docker infra (nodejs, artifactory)
    +$ ./setup_infra.sh
    +
    +# Kill & Run app
    +$ pkill -f "node app"
    +$ nohup node app &
    +
    +# Prepare environment variables
    +$ SC_CONTRACT_DOCKER_VERSION="..."
    +$ APP_IP="192.168.0.100"
    +$ APP_PORT="3000"
    +$ ARTIFACTORY_PORT="8081"
    +$ APPLICATION_BASE_URL="http://${APP_IP}:${APP_PORT}"
    +$ ARTIFACTORY_URL="http://${APP_IP}:${ARTIFACTORY_PORT}/artifactory/libs-release-local"
    +$ CURRENT_DIR="$( pwd )"
    +$ CURRENT_FOLDER_NAME=${PWD##*/}
    +$ PROJECT_VERSION="0.0.1.RELEASE"
    +
    +# Execute contract tests
    +$ docker run  --rm -e "APPLICATION_BASE_URL=${APPLICATION_BASE_URL}" -e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=${CURRENT_FOLDER_NAME}" -e "REPO_WITH_BINARIES_URL=${ARTIFACTORY_URL}" -e "PROJECT_VERSION=${PROJECT_VERSION}" -v "${CURRENT_DIR}/contracts/:/contracts:ro" -v "${CURRENT_DIR}/node_modules/spring-cloud-contract/output:/spring-cloud-contract-output/" springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}"
    +
    +# Kill app
    +$ pkill -f "node app"

    What will happen is that via bash scripts:

    • infrastructure will be set up (MongoDb, Artifactory). +In real life scenario you would just run the NodeJS application +with mocked database. In this example we want to show how we can +benefit from Spring Cloud Contract in no time.
    • due to those constraints the contracts also represent the +stateful situation

      • first request is a POST that causes data to get inserted to the database
      • second request is a GET that returns a list of data with 1 previously inserted element
    • the NodeJS application will be started (on port 3000)
    • contract tests will be generated via Docker and tests +will be executed against the running application

      • the contracts will be taken from /contracts folder.
      • the output of the test execution is available under +node_modules/spring-cloud-contract/output.
    • the stubs will be uploaded to Artifactory. You can check them out +under http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/ . +The stubs will be here http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/bookstore-0.0.1.RELEASE-stubs.jar.

    To see how the client side looks like check out the Section 92.9, “Stub Runner Docker” section.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_pr01.html b/Greenwich.SR5/multi/multi_pr01.html new file mode 100644 index 00000000..57479b26 --- /dev/null +++ b/Greenwich.SR5/multi/multi_pr01.html @@ -0,0 +1,11 @@ + + +

    Spring Cloud provides tools for developers to quickly build some of +the common patterns in distributed systems (e.g. configuration +management, service discovery, circuit breakers, intelligent routing, +micro-proxy, control bus). Coordination of +distributed systems leads to boiler plate patterns, and using Spring +Cloud developers can quickly stand up services and applications that +implement those patterns. They will work well in any distributed +environment, including the developer’s own laptop, bare metal data +centres, and managed platforms such as Cloud Foundry.

    Version: Greenwich.SR5

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_retrying-failed-requests.html b/Greenwich.SR5/multi/multi_retrying-failed-requests.html new file mode 100644 index 00000000..7925fc66 --- /dev/null +++ b/Greenwich.SR5/multi/multi_retrying-failed-requests.html @@ -0,0 +1,27 @@ + + + 20. Retrying Failed Requests

    20. Retrying Failed Requests

    Spring Cloud Netflix offers a variety of ways to make HTTP requests. +You can use a load balanced RestTemplate, Ribbon, or Feign. +No matter how you choose to create your HTTP requests, there is always a chance that a request may fail. +When a request fails, you may want to have the request be retried automatically. +To do so when using Sping Cloud Netflix, you need to include Spring Retry on your application’s classpath. +When Spring Retry is present, load-balanced RestTemplates, Feign, and Zuul automatically retry any failed requests (assuming your configuration allows doing so).

    20.1 BackOff Policies

    By default, no backoff policy is used when retrying requests. +If you would like to configure a backoff policy, you need to create a bean of type LoadBalancedRetryFactory and override the createBackOffPolicy method for a given service, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +    @Bean
    +    LoadBalancedRetryFactory retryFactory() {
    +        return new LoadBalancedRetryFactory() {
    +            @Override
    +            public BackOffPolicy createBackOffPolicy(String service) {
    +                return new ExponentialBackOffPolicy();
    +            }
    +        };
    +    }
    +}

    20.2 Configuration

    When you use Ribbon with Spring Retry, you can control the retry functionality by configuring certain Ribbon properties. +To do so, set the client.ribbon.MaxAutoRetries, client.ribbon.MaxAutoRetriesNextServer, and client.ribbon.OkToRetryOnAllOperations properties. +See the Ribbon documentation for a description of what these properties do.

    [Warning]Warning

    Enabling client.ribbon.OkToRetryOnAllOperations includes retrying POST requests, which can have an impact +on the server’s resources, due to the buffering of the request body.

    In addition, you may want to retry requests when certain status codes are returned in the response. +You can list the response codes you would like the Ribbon client to retry by setting the clientName.ribbon.retryableStatusCodes property, as shown in the following example:

    clientName:
    +  ribbon:
    +    retryableStatusCodes: 404,502

    You can also create a bean of type LoadBalancedRetryPolicy and implement the retryableStatusCode method to retry a request given the status code.

    20.2.1 Zuul

    You can turn off Zuul’s retry functionality by setting zuul.retryable to false. +You can also disable retry functionality on a route-by-route basis by setting zuul.routes.routename.retryable to false.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_schema-evolution.html b/Greenwich.SR5/multi/multi_schema-evolution.html new file mode 100644 index 00000000..c498eb94 --- /dev/null +++ b/Greenwich.SR5/multi/multi_schema-evolution.html @@ -0,0 +1,88 @@ + + + 33. Schema Evolution Support

    33. Schema Evolution Support

    Spring Cloud Stream provides support for schema evolution so that the data can be evolved over time and still work with older or newer producers and consumers and vice versa. +Most serialization models, especially the ones that aim for portability across different platforms and languages, rely on a schema that describes how the data is serialized in the binary payload. +In order to serialize the data and then to interpret it, both the sending and receiving sides must have access to a schema that describes the binary format. +In certain cases, the schema can be inferred from the payload type on serialization or from the target type on deserialization. +However, many applications benefit from having access to an explicit schema that describes the binary data format. +A schema registry lets you store schema information in a textual format (typically JSON) and makes that information accessible to various applications that need it to receive and send data in binary format. +A schema is referenceable as a tuple consisting of:

    • A subject that is the logical name of the schema
    • The schema version
    • The schema format, which describes the binary format of the data

    This following sections goes through the details of various components involved in schema evolution process.

    33.1 Schema Registry Client

    The client-side abstraction for interacting with schema registry servers is the SchemaRegistryClient interface, which has the following structure:

    public interface SchemaRegistryClient {
    +
    +    SchemaRegistrationResponse register(String subject, String format, String schema);
    +
    +    String fetch(SchemaReference schemaReference);
    +
    +    String fetch(Integer id);
    +
    +}

    Spring Cloud Stream provides out-of-the-box implementations for interacting with its own schema server and for interacting with the Confluent Schema Registry.

    A client for the Spring Cloud Stream schema registry can be configured by using the @EnableSchemaRegistryClient, as follows:

      @EnableBinding(Sink.class)
    +  @SpringBootApplication
    +  @EnableSchemaRegistryClient
    +  public static class AvroSinkApplication {
    +    ...
    +  }
    [Note]Note

    The default converter is optimized to cache not only the schemas from the remote server but also the parse() and toString() methods, which are quite expensive. +Because of this, it uses a DefaultSchemaRegistryClient that does not cache responses. +If you intend to change the default behavior, you can use the client directly on your code and override it to the desired outcome. +To do so, you have to add the property spring.cloud.stream.schemaRegistryClient.cached=true to your application properties.

    33.1.1 Schema Registry Client Properties

    The Schema Registry Client supports the following properties:

    spring.cloud.stream.schemaRegistryClient.endpoint
    The location of the schema-server. +When setting this, use a full URL, including protocol (http or https) , port, and context path.
    Default
    http://localhost:8990/
    spring.cloud.stream.schemaRegistryClient.cached
    Whether the client should cache schema server responses. +Normally set to false, as the caching happens in the message converter. +Clients using the schema registry client should set this to true.
    Default
    true

    33.2 Avro Schema Registry Client Message Converters

    For applications that have a SchemaRegistryClient bean registered with the application context, Spring Cloud Stream auto configures an Apache Avro message converter for schema management. +This eases schema evolution, as applications that receive messages can get easy access to a writer schema that can be reconciled with their own reader schema.

    For outbound messages, if the content type of the channel is set to application/*+avro, the MessageConverter is activated, as shown in the following example:

    spring.cloud.stream.bindings.output.contentType=application/*+avro

    During the outbound conversion, the message converter tries to infer the schema of each outbound messages (based on its type) and register it to a subject (based on the payload type) by using the SchemaRegistryClient. +If an identical schema is already found, then a reference to it is retrieved. +If not, the schema is registered, and a new version number is provided. +The message is sent with a contentType header by using the following scheme: application/[prefix].[subject].v[version]+avro, where prefix is configurable and subject is deduced from the payload type.

    For example, a message of the type User might be sent as a binary payload with a content type of application/vnd.user.v2+avro, where user is the subject and 2 is the version number.

    When receiving messages, the converter infers the schema reference from the header of the incoming message and tries to retrieve it. The schema is used as the writer schema in the deserialization process.

    33.2.1 Avro Schema Registry Message Converter Properties

    If you have enabled Avro based schema registry client by setting spring.cloud.stream.bindings.output.contentType=application/*+avro, you can customize the behavior of the registration by setting the following properties.

    spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled

    Enable if you want the converter to use reflection to infer a Schema from a POJO.

    Default: false

    spring.cloud.stream.schema.avro.readerSchema
    Avro compares schema versions by looking at a writer schema (origin payload) and a reader schema (your application payload). See the Avro documentation for more information. If set, this overrides any lookups at the schema server and uses the local schema as the reader schema. +Default: null
    spring.cloud.stream.schema.avro.schemaLocations

    Registers any .avsc files listed in this property with the Schema Server.

    Default: empty

    spring.cloud.stream.schema.avro.prefix

    The prefix to be used on the Content-Type header.

    Default: vnd

    33.3 Apache Avro Message Converters

    Spring Cloud Stream provides support for schema-based message converters through its spring-cloud-stream-schema module. +Currently, the only serialization format supported out of the box for schema-based message converters is Apache Avro, with more formats to be added in future versions.

    The spring-cloud-stream-schema module contains two types of message converters that can be used for Apache Avro serialization:

    • Converters that use the class information of the serialized or deserialized objects or a schema with a location known at startup.
    • Converters that use a schema registry. They locate the schemas at runtime and dynamically register new schemas as domain objects evolve.

    33.4 Converters with Schema Support

    The AvroSchemaMessageConverter supports serializing and deserializing messages either by using a predefined schema or by using the schema information available in the class (either reflectively or contained in the SpecificRecord). +If you provide a custom converter, then the default AvroSchemaMessageConverter bean is not created. The following example shows a custom converter:

    To use custom converters, you can simply add it to the application context, optionally specifying one or more MimeTypes with which to associate it. +The default MimeType is application/avro.

    If the target type of the conversion is a GenericRecord, a schema must be set.

    The following example shows how to configure a converter in a sink application by registering the Apache Avro MessageConverter without a predefined schema. +In this example, note that the mime type value is avro/bytes, not the default application/avro.

    @EnableBinding(Sink.class)
    +@SpringBootApplication
    +public static class SinkApplication {
    +
    +  ...
    +
    +  @Bean
    +  public MessageConverter userMessageConverter() {
    +      return new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes"));
    +  }
    +}

    Conversely, the following application registers a converter with a predefined schema (found on the classpath):

    @EnableBinding(Sink.class)
    +@SpringBootApplication
    +public static class SinkApplication {
    +
    +  ...
    +
    +  @Bean
    +  public MessageConverter userMessageConverter() {
    +      AvroSchemaMessageConverter converter = new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes"));
    +      converter.setSchemaLocation(new ClassPathResource("schemas/User.avro"));
    +      return converter;
    +  }
    +}

    33.5 Schema Registry Server

    Spring Cloud Stream provides a schema registry server implementation. +To use it, you can add the spring-cloud-stream-schema-server artifact to your project and use the @EnableSchemaRegistryServer annotation, which adds the schema registry server REST controller to your application. +This annotation is intended to be used with Spring Boot web applications, and the listening port of the server is controlled by the server.port property. +The spring.cloud.stream.schema.server.path property can be used to control the root path of the schema server (especially when it is embedded in other applications). +The spring.cloud.stream.schema.server.allowSchemaDeletion boolean property enables the deletion of a schema. By default, this is disabled.

    The schema registry server uses a relational database to store the schemas. +By default, it uses an embedded database. +You can customize the schema storage by using the Spring Boot SQL database and JDBC configuration options.

    The following example shows a Spring Boot application that enables the schema registry:

    @SpringBootApplication
    +@EnableSchemaRegistryServer
    +public class SchemaRegistryServerApplication {
    +    public static void main(String[] args) {
    +        SpringApplication.run(SchemaRegistryServerApplication.class, args);
    +    }
    +}

    33.5.1 Schema Registry Server API

    The Schema Registry Server API consists of the following operations:

    Registering a New Schema

    To register a new schema, send a POST request to the / endpoint.

    The / accepts a JSON payload with the following fields:

    • subject: The schema subject
    • format: The schema format
    • definition: The schema definition

    Its response is a schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Retrieving an Existing Schema by Subject, Format, and Version

    To retrieve an existing schema by subject, format, and version, send GET request to the /{subject}/{format}/{version} endpoint.

    Its response is a schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Retrieving an Existing Schema by Subject and Format

    To retrieve an existing schema by subject and format, send a GET request to the /subject/format endpoint.

    Its response is a list of schemas with each schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Retrieving an Existing Schema by ID

    To retrieve a schema by its ID, send a GET request to the /schemas/{id} endpoint.

    Its response is a schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Deleting a Schema by Subject, Format, and Version

    To delete a schema identified by its subject, format, and version, send a DELETE request to the /{subject}/{format}/{version} endpoint.

    Deleting a Schema by ID

    To delete a schema by its ID, send a DELETE request to the /schemas/{id} endpoint.

    Deleting a Schema by Subject

    DELETE /{subject}

    Delete existing schemas by their subject.

    [Note]Note

    This note applies to users of Spring Cloud Stream 1.1.0.RELEASE only. +Spring Cloud Stream 1.1.0.RELEASE used the table name, schema, for storing Schema objects. Schema is a keyword in a number of database implementations. +To avoid any conflicts in the future, starting with 1.1.1.RELEASE, we have opted for the name SCHEMA_REPOSITORY for the storage table. +Any Spring Cloud Stream 1.1.0.RELEASE users who upgrade should migrate their existing schemas to the new table before upgrading.

    33.5.2 Using Confluent’s Schema Registry

    The default configuration creates a DefaultSchemaRegistryClient bean. +If you want to use the Confluent schema registry, you need to create a bean of type ConfluentSchemaRegistryClient, which supersedes the one configured by default by the framework. The following example shows how to create such a bean:

    @Bean
    +public SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.stream.schemaRegistryClient.endpoint}") String endpoint){
    +  ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient();
    +  client.setEndpoint(endpoint);
    +  return client;
    +}
    [Note]Note

    The ConfluentSchemaRegistryClient is tested against Confluent platform version 4.0.0.

    33.6 Schema Registration and Resolution

    To better understand how Spring Cloud Stream registers and resolves new schemas and its use of Avro schema comparison features, we provide two separate subsections:

    33.6.1 Schema Registration Process (Serialization)

    The first part of the registration process is extracting a schema from the payload that is being sent over a channel. +Avro types such as SpecificRecord or GenericRecord already contain a schema, which can be retrieved immediately from the instance. +In the case of POJOs, a schema is inferred if the spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled property is set to true (the default).

    Figure 33.1. Schema Writer Resolution Process

    schema resolution

    Ones a schema is obtained, the converter loads its metadata (version) from the remote server. +First, it queries a local cache. If no result is found, it submits the data to the server, which replies with versioning information. +The converter always caches the results to avoid the overhead of querying the Schema Server for every new message that needs to be serialized.

    Figure 33.2. Schema Registration Process

    registration

    With the schema version information, the converter sets the contentType header of the message to carry the version information — for example: application/vnd.user.v1+avro.

    33.6.2 Schema Resolution Process (Deserialization)

    When reading messages that contain version information (that is, a contentType header with a scheme like the one described under Section 33.6.1, “Schema Registration Process (Serialization)”), the converter queries the Schema server to fetch the writer schema of the message. +Once it has found the correct schema of the incoming message, it retrieves the reader schema and, by using Avro’s schema resolution support, reads it into the reader definition (setting defaults and any missing properties).

    Figure 33.3. Schema Reading Resolution Process

    schema reading

    [Note]Note

    You should understand the difference between a writer schema (the application that wrote the message) and a reader schema (the receiving application). +We suggest taking a moment to read the Avro terminology and understand the process. +Spring Cloud Stream always fetches the writer schema to determine how to read a message. +If you want to get Avro’s schema evolution support working, you need to make sure that a readerSchema was properly set for your application.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-agent.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-agent.html new file mode 100644 index 00000000..552e8606 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-agent.html @@ -0,0 +1,3 @@ + + + 67. Consul Agent

    67. Consul Agent

    A Consul Agent client must be available to all Spring Cloud Consul applications. By default, the Agent client is expected to be at localhost:8500. See the Agent documentation for specifics on how to start an Agent client and how to connect to a cluster of Consul Agent Servers. For development, after you have installed consul, you may start a Consul Agent using the following command:

    ./src/main/bash/local_run_consul.sh

    This will start an agent in server mode on port 8500, with the ui available at http://localhost:8500

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-bus.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-bus.html new file mode 100644 index 00000000..fab000d4 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-bus.html @@ -0,0 +1,3 @@ + + + 71. Spring Cloud Bus with Consul

    71. Spring Cloud Bus with Consul

    71.1 How to activate

    To get started with the Consul Bus use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-bus. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    See the Spring Cloud Bus documentation for the available actuator endpoints and howto send custom messages.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-config.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-config.html new file mode 100644 index 00000000..4584a04d --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-config.html @@ -0,0 +1,38 @@ + + + 69. Distributed Configuration with Consul

    69. Distributed Configuration with Consul

    Consul provides a Key/Value Store for storing configuration and other metadata. Spring Cloud Consul Config is an alternative to the Config Server and Client. Configuration is loaded into the Spring Environment during the special "bootstrap" phase. Configuration is stored in the /config folder by default. Multiple PropertySource instances are created based on the application’s name and the active profiles that mimicks the Spring Cloud Config order of resolving properties. For example, an application with the name "testApp" and with the "dev" profile will have the following property sources created:

    config/testApp,dev/
    +config/testApp/
    +config/application,dev/
    +config/application/

    The most specific property source is at the top, with the least specific at the bottom. Properties in the config/application folder are applicable to all applications using consul for configuration. Properties in the config/testApp folder are only available to the instances of the service named "testApp".

    Configuration is currently read on startup of the application. Sending a HTTP POST to /refresh will cause the configuration to be reloaded. Section 69.3, “Config Watch” will also automatically detect changes and reload the application context.

    69.1 How to activate

    To get started with Consul Configuration use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-config. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    This will enable auto-configuration that will setup Spring Cloud Consul Config.

    69.2 Customizing

    Consul Config may be customized using the following properties:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      config:
    +        enabled: true
    +        prefix: configuration
    +        defaultContext: apps
    +        profileSeparator: '::'

    +

    • enabled setting this value to "false" disables Consul Config
    • prefix sets the base folder for configuration values
    • defaultContext sets the folder name used by all applications
    • profileSeparator sets the value of the separator used to separate the profile name in property sources with profiles

    69.3 Config Watch

    The Consul Config Watch takes advantage of the ability of consul to watch a key prefix. The Config Watch makes a blocking Consul HTTP API call to determine if any relevant configuration data has changed for the current application. If there is new configuration data a Refresh Event is published. This is equivalent to calling the /refresh actuator endpoint.

    To change the frequency of when the Config Watch is called change spring.cloud.consul.config.watch.delay. The default value is 1000, which is in milliseconds. The delay is the amount of time after the end of the previous invocation and the start of the next.

    To disable the Config Watch set spring.cloud.consul.config.watch.enabled=false.

    The watch uses a Spring TaskScheduler to schedule the call to consul. By default it is a ThreadPoolTaskScheduler with a poolSize of 1. To change the TaskScheduler, create a bean of type TaskScheduler named with the ConsulConfigAutoConfiguration.CONFIG_WATCH_TASK_SCHEDULER_NAME constant.

    69.4 YAML or Properties with Config

    It may be more convenient to store a blob of properties in YAML or Properties format as opposed to individual key/value pairs. Set the spring.cloud.consul.config.format property to YAML or PROPERTIES. For example to use YAML:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      config:
    +        format: YAML

    +

    YAML must be set in the appropriate data key in consul. Using the defaults above the keys would look like:

    config/testApp,dev/data
    +config/testApp/data
    +config/application,dev/data
    +config/application/data

    You could store a YAML document in any of the keys listed above.

    You can change the data key using spring.cloud.consul.config.data-key.

    69.5 git2consul with Config

    git2consul is a Consul community project that loads files from a git repository to individual keys into Consul. By default the names of the keys are names of the files. YAML and Properties files are supported with file extensions of .yml and .properties respectively. Set the spring.cloud.consul.config.format property to FILES. For example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      config:
    +        format: FILES

    +

    Given the following keys in /config, the development profile and an application name of foo:

    .gitignore
    +application.yml
    +bar.properties
    +foo-development.properties
    +foo-production.yml
    +foo.properties
    +master.ref

    the following property sources would be created:

    config/foo-development.properties
    +config/foo.properties
    +config/application.yml

    The value of each key needs to be a properly formatted YAML or Properties file.

    69.6 Fail Fast

    It may be convenient in certain circumstances (like local development or certain test scenarios) to not fail if consul isn’t available for configuration. Setting spring.cloud.consul.config.failFast=false in bootstrap.yml will cause the configuration module to log a warning rather than throw an exception. This will allow the application to continue startup normally.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-discovery.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-discovery.html new file mode 100644 index 00000000..a08b9516 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-discovery.html @@ -0,0 +1,109 @@ + + + 68. Service Discovery with Consul

    68. Service Discovery with Consul

    Service Discovery is one of the key tenets of a microservice based architecture. Trying to hand configure each client or some form of convention can be very difficult to do and can be very brittle. Consul provides Service Discovery services via an HTTP API and DNS. Spring Cloud Consul leverages the HTTP API for service registration and discovery. This does not prevent non-Spring Cloud applications from leveraging the DNS interface. Consul Agents servers are run in a cluster that communicates via a gossip protocol and uses the Raft consensus protocol.

    68.1 How to activate

    To activate Consul Service Discovery use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-discovery. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    68.2 Registering with Consul

    When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An HTTP Check is created by default that Consul hits the /health endpoint every 10 seconds. If the health check fails, the service instance is marked as critical.

    Example Consul client:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello world";
    +    }
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}

    (i.e. utterly normal Spring Boot app). If the Consul client is located somewhere other than localhost:8500, the configuration is required to locate the client. Example:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      host: localhost
    +      port: 8500

    +

    [Caution]Caution

    If you use Spring Cloud Consul Config, the above values will need to be placed in bootstrap.yml instead of application.yml.

    The default service name, instance id and port, taken from the Environment, are ${spring.application.name}, the Spring Context ID and ${server.port} respectively.

    To disable the Consul Discovery Client you can set spring.cloud.consul.discovery.enabled to false. Consul Discovery Client will also be disabled when spring.cloud.discovery.enabled is set to false.

    To disable the service registration you can set spring.cloud.consul.discovery.register to false.

    68.2.1 Registering Management as a Separate Service

    When management server port is set to something different than the application port, by setting management.server.port property, management service will be registered as a separate service than the application service. For example:

    application.yml.  +

    spring:
    +  application:
    +    name: myApp
    +management:
    +  server:
    +    port: 4452

    +

    Above configuration will register following 2 services:

    • Application Service:
    ID: myApp
    +Name: myApp
    • Management Service:
    ID: myApp-management
    +Name: myApp-management

    Management service will inherit its instanceId and serviceName from the application service. For example:

    application.yml.  +

    spring:
    +  application:
    +    name: myApp
    +management:
    +  server:
    +    port: 4452
    +spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        instance-id: custom-service-id
    +        serviceName: myprefix-${spring.application.name}

    +

    Above configuration will register following 2 services:

    • Application Service:
    ID: custom-service-id
    +Name: myprefix-myApp
    • Management Service:
    ID: custom-service-id-management
    +Name: myprefix-myApp-management

    Further customization is possible via following properties:

    /** Port to register the management service under (defaults to management port) */
    +spring.cloud.consul.discovery.management-port
    +
    +/** Suffix to use when registering management service (defaults to "management" */
    +spring.cloud.consul.discovery.management-suffix
    +
    +/** Tags to use when registering management service (defaults to "management" */
    +spring.cloud.consul.discovery.management-tags

    68.3 HTTP Health Check

    The health check for a Consul instance defaults to "/health", which is the default locations of a useful endpoint in a Spring Boot Actuator application. You need to change these, even for an Actuator application if you use a non-default context path or servlet path (e.g. server.servletPath=/foo) or management endpoint path (e.g. management.server.servlet.context-path=/admin). The interval that Consul uses to check the health endpoint may also be configured. "10s" and "1m" represent 10 seconds and 1 minute respectively. Example:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        healthCheckPath: ${management.server.servlet.context-path}/health
    +        healthCheckInterval: 15s

    +

    You can disable the health check by setting management.health.consul.enabled=false.

    68.3.1 Metadata and Consul tags

    Consul does not yet support metadata on services. Spring Cloud’s ServiceInstance has a Map<String, String> metadata field. Spring Cloud Consul uses Consul tags to approximate metadata until Consul officially supports metadata. Tags with the form key=value will be split and used as a Map key and value respectively. Tags without the equal = sign, will be used as both the key and value.

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        tags: foo=bar, baz

    +

    The above configuration will result in a map with foo→bar and baz→baz.

    68.3.2 Making the Consul Instance ID Unique

    By default a consul instance is registered with an ID that is equal to its Spring Application Context ID. By default, the Spring Application Context ID is ${spring.application.name}:comma,separated,profiles:${server.port}. For most cases, this will allow multiple instances of one service to run on one machine. If further uniqueness is required, Using Spring Cloud you can override this by providing a unique identifier in spring.cloud.consul.discovery.instanceId. For example:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}

    +

    With this metadata, and multiple service instances deployed on localhost, the random value will kick in there to make the instance unique. In Cloudfoundry the vcap.application.instance_id will be populated automatically in a Spring Boot application, so the random value will not be needed.

    68.3.3 Applying Headers to Health Check Requests

    Headers can be applied to health check requests. For example, if you’re trying to register a Spring Cloud Config server that uses Vault Backend:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        health-check-headers:
    +          X-Config-Token: 6442e58b-d1ea-182e-cfa5-cf9cddef0722

    +

    According to the HTTP standard, each header can have more than one values, in which case, an array can be supplied:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        health-check-headers:
    +          X-Config-Token:
    +            - "6442e58b-d1ea-182e-cfa5-cf9cddef0722"
    +            - "Some other value"

    +

    68.4 Looking up services

    68.4.1 Using Ribbon

    Spring Cloud has support for Feign (a REST client builder) and also Spring RestTemplate +for looking up services using the logical service names/ids instead of physical URLs. Both Feign and the discovery-aware RestTemplate utilize Ribbon for client-side load balancing.

    If you want to access service STORES using the RestTemplate simply declare:

    @LoadBalanced
    +@Bean
    +public RestTemplate loadbalancedRestTemplate() {
    +     new RestTemplate();
    +}

    and use it like this (notice how we use the STORES service name/id from Consul instead of a fully qualified domainname):

    @Autowired
    +RestTemplate restTemplate;
    +
    +public String getFirstProduct() {
    +   return this.restTemplate.getForObject("https://STORES/products/1", String.class);
    +}

    If you have Consul clusters in multiple datacenters and you want to access a service in another datacenter a service name/id alone is not enough. In that case +you use property spring.cloud.consul.discovery.datacenters.STORES=dc-west where STORES is the service name/id and dc-west is the datacenter +where the STORES service lives.

    68.4.2 Using the DiscoveryClient

    You can also use the org.springframework.cloud.client.discovery.DiscoveryClient which provides a simple API for discovery clients that is not specific to Netflix, e.g.

    @Autowired
    +private DiscoveryClient discoveryClient;
    +
    +public String serviceUrl() {
    +    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    +    if (list != null && list.size() > 0 ) {
    +        return list.get(0).getUri();
    +    }
    +    return null;
    +}

    68.5 Consul Catalog Watch

    The Consul Catalog Watch takes advantage of the ability of consul to watch services. The Catalog Watch makes a blocking Consul HTTP API call to determine if any services have changed. If there is new service data a Heartbeat Event is published.

    To change the frequency of when the Config Watch is called change spring.cloud.consul.config.discovery.catalog-services-watch-delay. The default value is 1000, which is in milliseconds. The delay is the amount of time after the end of the previous invocation and the start of the next.

    To disable the Catalog Watch set spring.cloud.consul.discovery.catalogServicesWatch.enabled=false.

    The watch uses a Spring TaskScheduler to schedule the call to consul. By default it is a ThreadPoolTaskScheduler with a poolSize of 1. To change the TaskScheduler, create a bean of type TaskScheduler named with the ConsulDiscoveryClientConfiguration.CATALOG_WATCH_TASK_SCHEDULER_NAME constant.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-hystrix.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-hystrix.html new file mode 100644 index 00000000..32170189 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-hystrix.html @@ -0,0 +1,3 @@ + + + 72. Circuit Breaker with Hystrix

    72. Circuit Breaker with Hystrix

    Applications can use the Hystrix Circuit Breaker provided by the Spring Cloud Netflix project by including this starter in the projects pom.xml: spring-cloud-starter-hystrix. Hystrix doesn’t depend on the Netflix Discovery Client. The @EnableHystrix annotation should be placed on a configuration class (usually the main class). Then methods can be annotated with @HystrixCommand to be protected by a circuit breaker. See the documentation for more details.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-install.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-install.html new file mode 100644 index 00000000..b2697e1f --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-install.html @@ -0,0 +1,3 @@ + + + 66. Install Consul

    66. Install Consul

    Please see the installation documentation for instructions on how to install Consul.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-retry.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-retry.html new file mode 100644 index 00000000..f2007fa2 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-retry.html @@ -0,0 +1,11 @@ + + + 70. Consul Retry

    70. Consul Retry

    If you expect that the consul agent may occasionally be unavailable when +your app starts, you can ask it to keep trying after a failure. You need to add +spring-retry and spring-boot-starter-aop to your classpath. The default +behaviour is to retry 6 times with an initial backoff interval of 1000ms and an +exponential multiplier of 1.1 for subsequent backoffs. You can configure these +properties (and others) using spring.cloud.consul.retry.* configuration properties. +This works with both Spring Cloud Consul Config and Discovery registration.

    [Tip]Tip

    To take full control of the retry add a @Bean of type +RetryOperationsInterceptor with id "consulRetryInterceptor". Spring +Retry has a RetryInterceptorBuilder that makes it easy to create one.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-consul-turbine.html b/Greenwich.SR5/multi/multi_spring-cloud-consul-turbine.html new file mode 100644 index 00000000..93661d0f --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-consul-turbine.html @@ -0,0 +1,27 @@ + + + 73. Hystrix metrics aggregation with Turbine and Consul

    73. Hystrix metrics aggregation with Turbine and Consul

    Turbine (provided by the Spring Cloud Netflix project), aggregates multiple instances Hystrix metrics streams, so the dashboard can display an aggregate view. Turbine uses the DiscoveryClient interface to lookup relevant instances. To use Turbine with Spring Cloud Consul, configure the Turbine application in a manner similar to the following examples:

    pom.xml.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-netflix-turbine</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    +</dependency>

    +

    Notice that the Turbine dependency is not a starter. The turbine starter includes support for Netflix Eureka.

    application.yml.  +

    spring.application.name: turbine
    +applications: consulhystrixclient
    +turbine:
    +  aggregator:
    +    clusterConfig: ${applications}
    +  appConfig: ${applications}

    +

    The clusterConfig and appConfig sections must match, so it’s useful to put the comma-separated list of service ID’s into a separate configuration property.

    Turbine.java.  +

    @EnableTurbine
    +@SpringBootApplication
    +public class Turbine {
    +    public static void main(String[] args) {
    +        SpringApplication.run(DemoturbinecommonsApplication.class, args);
    +    }
    +}

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-eureka-server.html b/Greenwich.SR5/multi/multi_spring-cloud-eureka-server.html new file mode 100644 index 00000000..69e9519f --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-eureka-server.html @@ -0,0 +1,121 @@ + + + 12. Service Discovery: Eureka Server

    12. Service Discovery: Eureka Server

    This section describes how to set up a Eureka server.

    12.1 How to Include Eureka Server

    To include Eureka Server in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-eureka-server. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    [Note]Note

    If your project already uses Thymeleaf as its template engine, the Freemarker templates of the Eureka server may not be loaded correctly. In this case it is necessary to configure the template loader manually:

    application.yml.  +

    spring:
    +  freemarker:
    +    template-loader-path: classpath:/templates/
    +    prefer-file-system-access: false

    +

    12.2 How to Run a Eureka Server

    The following example shows a minimal Eureka server:

    @SpringBootApplication
    +@EnableEurekaServer
    +public class Application {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}

    The server has a home page with a UI and HTTP API endpoints for the normal Eureka functionality under /eureka/*.

    The following links have some Eureka background reading: flux capacitor and google group discussion.

    [Tip]Tip

    Due to Gradle’s dependency resolution rules and the lack of a parent bom feature, depending on spring-cloud-starter-netflix-eureka-server can cause failures on application startup. +To remedy this issue, add the Spring Boot Gradle plugin and import the Spring cloud starter parent bom as follows:

    build.gradle.  +

    buildscript {
    +  dependencies {
    +    classpath("org.springframework.boot:spring-boot-gradle-plugin:{spring-boot-docs-version}")
    +  }
    +}
    +
    +apply plugin: "spring-boot"
    +
    +dependencyManagement {
    +  imports {
    +    mavenBom "org.springframework.cloud:spring-cloud-dependencies:{spring-cloud-version}"
    +  }
    +}

    +

    12.3 High Availability, Zones and Regions

    The Eureka server does not have a back end store, but the service instances in the registry all have to send heartbeats to keep their registrations up to date (so this can be done in memory). +Clients also have an in-memory cache of Eureka registrations (so they do not have to go to the registry for every request to a service).

    By default, every Eureka server is also a Eureka client and requires (at least one) service URL to locate a peer. +If you do not provide it, the service runs and works, but it fills your logs with a lot of noise about not being able to register with the peer.

    See also below for details of Ribbon support on the client side for Zones and Regions.

    12.4 Standalone Mode

    The combination of the two caches (client and server) and the heartbeats make a standalone Eureka server fairly resilient to failure, as long as there is some sort of monitor or elastic runtime (such as Cloud Foundry) keeping it alive. +In standalone mode, you might prefer to switch off the client side behavior so that it does not keep trying and failing to reach its peers. +The following example shows how to switch off the client-side behavior:

    application.yml (Standalone Eureka Server).  +

    server:
    +  port: 8761
    +
    +eureka:
    +  instance:
    +    hostname: localhost
    +  client:
    +    registerWithEureka: false
    +    fetchRegistry: false
    +    serviceUrl:
    +      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

    +

    Notice that the serviceUrl is pointing to the same host as the local instance.

    12.5 Peer Awareness

    Eureka can be made even more resilient and available by running multiple instances and asking them to register with each other. +In fact, this is the default behavior, so all you need to do to make it work is add a valid serviceUrl to a peer, as shown in the following example:

    application.yml (Two Peer Aware Eureka Servers).  +

    ---
    +spring:
    +  profiles: peer1
    +eureka:
    +  instance:
    +    hostname: peer1
    +  client:
    +    serviceUrl:
    +      defaultZone: http://peer2/eureka/
    +
    +---
    +spring:
    +  profiles: peer2
    +eureka:
    +  instance:
    +    hostname: peer2
    +  client:
    +    serviceUrl:
    +      defaultZone: http://peer1/eureka/

    +

    In the preceding example, we have a YAML file that can be used to run the same server on two hosts (peer1 and peer2) by running it in different Spring profiles. +You could use this configuration to test the peer awareness on a single host (there is not much value in doing that in production) by manipulating /etc/hosts to resolve the host names. +In fact, the eureka.instance.hostname is not needed if you are running on a machine that knows its own hostname (by default, it is looked up by using java.net.InetAddress).

    You can add multiple peers to a system, and, as long as they are all connected to each other by at least one edge, they synchronize +the registrations amongst themselves. +If the peers are physically separated (inside a data center or between multiple data centers), then the system can, in principle, survive split-brain type failures. +You can add multiple peers to a system, and as long as they are all +directly connected to each other, they will synchronize +the registrations amongst themselves.

    application.yml (Three Peer Aware Eureka Servers).  +

    eureka:
    +  client:
    +    serviceUrl:
    +      defaultZone: http://peer1/eureka/,http://peer2/eureka/,http://peer3/eureka/
    +
    +---
    +spring:
    +  profiles: peer1
    +eureka:
    +  instance:
    +    hostname: peer1
    +
    +---
    +spring:
    +  profiles: peer2
    +eureka:
    +  instance:
    +    hostname: peer2
    +
    +---
    +spring:
    +  profiles: peer3
    +eureka:
    +  instance:
    +    hostname: peer3

    +

    12.6 When to Prefer IP Address

    In some cases, it is preferable for Eureka to advertise the IP addresses of services rather than the hostname. +Set eureka.instance.preferIpAddress to true and, when the application registers with eureka, it uses its IP address rather than its hostname.

    [Tip]Tip

    If the hostname cannot be determined by Java, then the IP address is sent to Eureka. +Only explict way of setting the hostname is by setting eureka.instance.hostname property. +You can set your hostname at the run-time by using an environment variable — for example, eureka.instance.hostname=${HOST_NAME}.

    12.7 Securing The Eureka Server

    You can secure your Eureka server simply by adding Spring Security to your +server’s classpath via spring-boot-starter-security. By default when Spring Security is on the classpath it will require that +a valid CSRF token be sent with every request to the app. Eureka clients will not generally possess a valid +cross site request forgery (CSRF) token you will need to disable this requirement for the /eureka/** endpoints. +For example:

    @EnableWebSecurity
    +class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    +
    +    @Override
    +    protected void configure(HttpSecurity http) throws Exception {
    +        http.csrf().ignoringAntMatchers("/eureka/**");
    +        super.configure(http);
    +    }
    +}

    For more information on CSRF see the Spring Security documentation.

    A demo Eureka Server can be found in the Spring Cloud Samples repo.

    12.8 JDK 11 Support

    The JAXB modules which the Eureka server depends upon were removed in JDK 11. If you intend to use JDK 11 +when running a Eureka server you must include these dependencies in your POM or Gradle file.

    <dependency>
    +	<groupId>org.glassfish.jaxb</groupId>
    +	<artifactId>jaxb-runtime</artifactId>
    +</dependency>
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-feign.html b/Greenwich.SR5/multi/multi_spring-cloud-feign.html new file mode 100644 index 00000000..8118ad45 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-feign.html @@ -0,0 +1,221 @@ + + + 23. Declarative REST Client: Feign

    23. Declarative REST Client: Feign

    Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign.

    23.1 How to Include Feign

    To include Feign in your project use the starter with group org.springframework.cloud +and artifact id spring-cloud-starter-openfeign. See the Spring Cloud Project page +for details on setting up your build system with the current Spring Cloud Release Train.

    Example spring boot app

    @SpringBootApplication
    +@EnableFeignClients
    +public class Application {
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(Application.class, args);
    +    }
    +
    +}

    StoreClient.java.  +

    @FeignClient("stores")
    +public interface StoreClient {
    +    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    +    List<Store> getStores();
    +
    +    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    +    Store update(@PathVariable("storeId") Long storeId, Store store);
    +}

    +

    In the @FeignClient annotation the String value ("stores" above) is +an arbitrary client name, which is used to create a Ribbon load +balancer (see below for details of Ribbon +support). You can also specify a URL using the url attribute +(absolute value or just a hostname). The name of the bean in the +application context is the fully qualified name of the interface. +To specify your own alias value you can use the qualifier value +of the @FeignClient annotation.

    The Ribbon client above will want to discover the physical addresses +for the "stores" service. If your application is a Eureka client then +it will resolve the service in the Eureka service registry. If you +don’t want to use Eureka, you can simply configure a list of servers +in your external configuration (see +above for example).

    23.2 Overriding Feign Defaults

    A central concept in Spring Cloud’s Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the @FeignClient annotation. Spring Cloud creates a new ensemble as an +ApplicationContext on demand for each named client using FeignClientsConfiguration. This contains (amongst other things) an feign.Decoder, a feign.Encoder, and a feign.Contract. +It is possible to override the name of that ensemble by using the contextId +attribute of the @FeignClient annotation.

    Spring Cloud lets you take full control of the feign client by declaring additional configuration (on top of the FeignClientsConfiguration) using @FeignClient. Example:

    @FeignClient(name = "stores", configuration = FooConfiguration.class)
    +public interface StoreClient {
    +    //..
    +}

    In this case the client is composed from the components already in FeignClientsConfiguration together with any in FooConfiguration (where the latter will override the former).

    [Note]Note

    FooConfiguration does not need to be annotated with @Configuration. However, if it is, then take care to exclude it from any @ComponentScan that would otherwise include this configuration as it will become the default source for feign.Decoder, feign.Encoder, feign.Contract, etc., when specified. This can be avoided by putting it in a separate, non-overlapping package from any @ComponentScan or @SpringBootApplication, or it can be explicitly excluded in @ComponentScan.

    [Note]Note

    The serviceId attribute is now deprecated in favor of the name attribute.

    [Note]Note

    Using contextId attribute of the @FeignClient annotation in addition to changing the name of +the ApplicationContext ensemble, it will override the alias of the client name +and it will be used as part of the name of the configuration bean created for that client.

    [Warning]Warning

    Previously, using the url attribute, did not require the name attribute. Using name is now required.

    Placeholders are supported in the name and url attributes.

    @FeignClient(name = "${feign.name}", url = "${feign.url}")
    +public interface StoreClient {
    +    //..
    +}

    Spring Cloud Netflix provides the following beans by default for feign (BeanType beanName: ClassName):

    • Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder)
    • Encoder feignEncoder: SpringEncoder
    • Logger feignLogger: Slf4jLogger
    • Contract feignContract: SpringMvcContract
    • Feign.Builder feignBuilder: HystrixFeign.Builder
    • Client feignClient: if Ribbon is enabled it is a LoadBalancerFeignClient, otherwise the default feign client is used.

    The OkHttpClient and ApacheHttpClient feign clients can be used by setting feign.okhttp.enabled or feign.httpclient.enabled to true, respectively, and having them on the classpath. +You can customize the HTTP client used by providing a bean of either ClosableHttpClient when using Apache or OkHttpClient when using OK HTTP.

    Spring Cloud Netflix does not provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client:

    • Logger.Level
    • Retryer
    • ErrorDecoder
    • Request.Options
    • Collection<RequestInterceptor>
    • SetterFactory

    Creating a bean of one of those type and placing it in a @FeignClient configuration (such as FooConfiguration above) allows you to override each one of the beans described. Example:

    @Configuration
    +public class FooConfiguration {
    +    @Bean
    +    public Contract feignContract() {
    +        return new feign.Contract.Default();
    +    }
    +
    +    @Bean
    +    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
    +        return new BasicAuthRequestInterceptor("user", "password");
    +    }
    +}

    This replaces the SpringMvcContract with feign.Contract.Default and adds a RequestInterceptor to the collection of RequestInterceptor.

    @FeignClient also can be configured using configuration properties.

    application.yml

    feign:
    +  client:
    +    config:
    +      feignName:
    +        connectTimeout: 5000
    +        readTimeout: 5000
    +        loggerLevel: full
    +        errorDecoder: com.example.SimpleErrorDecoder
    +        retryer: com.example.SimpleRetryer
    +        requestInterceptors:
    +          - com.example.FooRequestInterceptor
    +          - com.example.BarRequestInterceptor
    +        decode404: false
    +        encoder: com.example.SimpleEncoder
    +        decoder: com.example.SimpleDecoder
    +        contract: com.example.SimpleContract

    Default configurations can be specified in the @EnableFeignClients attribute defaultConfiguration in a similar manner as described above. The difference is that this configuration will apply to all feign clients.

    If you prefer using configuration properties to configured all @FeignClient, you can create configuration properties with default feign name.

    application.yml

    feign:
    +  client:
    +    config:
    +      default:
    +        connectTimeout: 5000
    +        readTimeout: 5000
    +        loggerLevel: basic

    If we create both @Configuration bean and configuration properties, configuration properties will win. +It will override @Configuration values. But if you want to change the priority to @Configuration, +you can change feign.client.default-to-properties to false.

    [Note]Note

    If you need to use ThreadLocal bound variables in your RequestInterceptor`s you will need to either set the +thread isolation strategy for Hystrix to `SEMAPHORE or disable Hystrix in Feign.

    application.yml

    # To disable Hystrix in Feign
    +feign:
    +  hystrix:
    +    enabled: false
    +
    +# To set thread isolation to SEMAPHORE
    +hystrix:
    +  command:
    +    default:
    +      execution:
    +        isolation:
    +          strategy: SEMAPHORE

    If we want to create multiple feign clients with the same name or url +so that they would point to the same server but each with a different custom configuration then +we have to use contextId attribute of the @FeignClient in order to avoid name +collision of these configuration beans.

    @FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
    +public interface FooClient {
    +    //..
    +}
    @FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
    +public interface BarClient {
    +    //..
    +}

    23.3 Creating Feign Clients Manually

    In some cases it might be necessary to customize your Feign Clients in a way that is not +possible using the methods above. In this case you can create Clients using the +Feign Builder API. Below is an example +which creates two Feign Clients with the same interface but configures each one with +a separate request interceptor.

    @Import(FeignClientsConfiguration.class)
    +class FooController {
    +
    +	private FooClient fooClient;
    +
    +	private FooClient adminClient;
    +
    +    	@Autowired
    +	public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
    +		this.fooClient = Feign.builder().client(client)
    +				.encoder(encoder)
    +				.decoder(decoder)
    +				.contract(contract)
    +				.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
    +				.target(FooClient.class, "http://PROD-SVC");
    +
    +		this.adminClient = Feign.builder().client(client)
    +				.encoder(encoder)
    +				.decoder(decoder)
    +				.contract(contract)
    +				.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
    +				.target(FooClient.class, "http://PROD-SVC");
    +    }
    +}
    [Note]Note

    In the above example FeignClientsConfiguration.class is the default configuration +provided by Spring Cloud Netflix.

    [Note]Note

    PROD-SVC is the name of the service the Clients will be making requests to.

    [Note]Note

    The Feign Contract object defines what annotations and values are valid on interfaces. The +autowired Contract bean provides supports for SpringMVC annotations, instead of +the default Feign native annotations.

    23.4 Feign Hystrix Support

    If Hystrix is on the classpath and feign.hystrix.enabled=true, Feign will wrap all methods with a circuit breaker. Returning a com.netflix.hystrix.HystrixCommand is also available. This lets you use reactive patterns (with a call to .toObservable() or .observe() or asynchronous use (with a call to .queue()).

    To disable Hystrix support on a per-client basis create a vanilla Feign.Builder with the "prototype" scope, e.g.:

    @Configuration
    +public class FooConfiguration {
    +    	@Bean
    +	@Scope("prototype")
    +	public Feign.Builder feignBuilder() {
    +		return Feign.builder();
    +	}
    +}
    [Warning]Warning

    Prior to the Spring Cloud Dalston release, if Hystrix was on the classpath Feign would have wrapped +all methods in a circuit breaker by default. This default behavior was changed in Spring Cloud Dalston in +favor for an opt-in approach.

    23.5 Feign Hystrix Fallbacks

    Hystrix supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error. To enable fallbacks for a given @FeignClient set the fallback attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.

    @FeignClient(name = "hello", fallback = HystrixClientFallback.class)
    +protected interface HystrixClient {
    +    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    +    Hello iFailSometimes();
    +}
    +
    +static class HystrixClientFallback implements HystrixClient {
    +    @Override
    +    public Hello iFailSometimes() {
    +        return new Hello("fallback");
    +    }
    +}

    If one needs access to the cause that made the fallback trigger, one can use the fallbackFactory attribute inside @FeignClient.

    @FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
    +protected interface HystrixClient {
    +	@RequestMapping(method = RequestMethod.GET, value = "/hello")
    +	Hello iFailSometimes();
    +}
    +
    +@Component
    +static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
    +	@Override
    +	public HystrixClient create(Throwable cause) {
    +		return new HystrixClient() {
    +			@Override
    +			public Hello iFailSometimes() {
    +				return new Hello("fallback; reason was: " + cause.getMessage());
    +			}
    +		};
    +	}
    +}
    [Warning]Warning

    There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return com.netflix.hystrix.HystrixCommand and rx.Observable.

    23.6 Feign and @Primary

    When using Feign with Hystrix fallbacks, there are multiple beans in the ApplicationContext of the same type. This will cause @Autowired to not work because there isn’t exactly one bean, or one marked as primary. To work around this, Spring Cloud Netflix marks all Feign instances as @Primary, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the primary attribute of @FeignClient to false.

    @FeignClient(name = "hello", primary = false)
    +public interface HelloClient {
    +	// methods here
    +}

    23.7 Feign Inheritance Support

    Feign supports boilerplate apis via single-inheritance interfaces. +This allows grouping common operations into convenient base interfaces.

    UserService.java.  +

    public interface UserService {
    +
    +    @RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
    +    User getUser(@PathVariable("id") long id);
    +}

    +

    UserResource.java.  +

    @RestController
    +public class UserResource implements UserService {
    +
    +}

    +

    UserClient.java.  +

    package project.user;
    +
    +@FeignClient("users")
    +public interface UserClient extends UserService {
    +
    +}

    +

    [Note]Note

    It is generally not advisable to share an interface between a +server and a client. It introduces tight coupling, and also actually +doesn’t work with Spring MVC in its current form (method parameter +mapping is not inherited).

    23.8 Feign request/response compression

    You may consider enabling the request or response GZIP compression for your +Feign requests. You can do this by enabling one of the properties:

    feign.compression.request.enabled=true
    +feign.compression.response.enabled=true

    Feign request compression gives you settings similar to what you may set for your web server:

    feign.compression.request.enabled=true
    +feign.compression.request.mime-types=text/xml,application/xml,application/json
    +feign.compression.request.min-request-size=2048

    These properties allow you to be selective about the compressed media types and minimum request threshold length.

    23.9 Feign logging

    A logger is created for each Feign client created. By default the name of the logger is the full class name of the interface used to create the Feign client. Feign logging only responds to the DEBUG level.

    application.yml.  +

    logging.level.project.user.UserClient: DEBUG

    +

    The Logger.Level object that you may configure per client, tells Feign how much to log. Choices are:

    • NONE, No logging (DEFAULT).
    • BASIC, Log only the request method and URL and the response status code and execution time.
    • HEADERS, Log the basic information along with request and response headers.
    • FULL, Log the headers, body, and metadata for both requests and responses.

    For example, the following would set the Logger.Level to FULL:

    @Configuration
    +public class FooConfiguration {
    +    @Bean
    +    Logger.Level feignLoggerLevel() {
    +        return Logger.Level.FULL;
    +    }
    +}

    23.10 Feign @QueryMap support

    The OpenFeign @QueryMap annotation provides support for POJOs to be used as +GET parameter maps. Unfortunately, the default OpenFeign QueryMap annotation is +incompatible with Spring because it lacks a value property.

    Spring Cloud OpenFeign provides an equivalent @SpringQueryMap annotation, which +is used to annotate a POJO or Map parameter as a query parameter map.

    For example, the Params class defines parameters param1 and param2:

    // Params.java
    +public class Params {
    +    private String param1;
    +    private String param2;
    +
    +    // [Getters and setters omitted for brevity]
    +}

    The following feign client uses the Params class by using the @SpringQueryMap annotation:

    @FeignClient("demo")
    +public class DemoTemplate {
    +
    +    @GetMapping(path = "/demo")
    +    String demoEndpoint(@SpringQueryMap Params params);
    +}

    23.11 Troubleshooting

    23.11.1 Early Initialization Errors

    Depending on how you are using your Feign clients you may see initialization errors when starting your application. +To work around this problem you can use an ObjectProvider when autowiring your client.

    @Autowired
    +ObjectProvider<TestFeginClient> testFeginClient;
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-gcp-core.html b/Greenwich.SR5/multi/multi_spring-cloud-gcp-core.html new file mode 100644 index 00000000..d6441ce2 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-gcp-core.html @@ -0,0 +1,23 @@ + + + 154. Spring Cloud GCP Core

    154. Spring Cloud GCP Core

    Each Spring Cloud GCP module uses GcpProjectIdProvider and CredentialsProvider to get the GCP project ID and access credentials.

    Spring Cloud GCP provides a Spring Boot starter to auto-configure the core components.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter'
    +}

    154.1 Project ID

    GcpProjectIdProvider is a functional interface that returns a GCP project ID string.

    public interface GcpProjectIdProvider {
    +	String getProjectId();
    +}

    The Spring Cloud GCP starter auto-configures a GcpProjectIdProvider. +If a spring.cloud.gcp.project-id property is specified, the provided GcpProjectIdProvider returns that property value.

    spring.cloud.gcp.project-id=my-gcp-project-id

    Otherwise, the project ID is discovered based on an +ordered list of rules:

    1. The project ID specified by the GOOGLE_CLOUD_PROJECT environment variable
    2. The Google App Engine project ID
    3. The project ID specified in the JSON credentials file pointed by the GOOGLE_APPLICATION_CREDENTIALS environment variable
    4. The Google Cloud SDK project ID
    5. The Google Compute Engine project ID, from the Google Compute Engine Metadata Server

    154.2 Credentials

    CredentialsProvider is a functional interface that returns the credentials to authenticate and authorize calls to Google Cloud Client Libraries.

    public interface CredentialsProvider {
    +  Credentials getCredentials() throws IOException;
    +}

    The Spring Cloud GCP starter auto-configures a CredentialsProvider. +It uses the spring.cloud.gcp.credentials.location property to locate the OAuth2 private key of a Google service account. +Keep in mind this property is a Spring Resource, so the credentials file can be obtained from a number of different locations such as the file system, classpath, URL, etc. +The next example specifies the credentials location property in the file system.

    spring.cloud.gcp.credentials.location=file:/usr/local/key.json

    Alternatively, you can set the credentials by directly specifying the spring.cloud.gcp.credentials.encoded-key property. +The value should be the base64-encoded account private key in JSON format.

    If that credentials aren’t specified through properties, the starter tries to discover credentials from a number of places:

    1. Credentials file pointed to by the GOOGLE_APPLICATION_CREDENTIALS environment variable
    2. Credentials provided by the Google Cloud SDK gcloud auth application-default login command
    3. Google App Engine built-in credentials
    4. Google Cloud Shell built-in credentials
    5. Google Compute Engine built-in credentials

    If your app is running on Google App Engine or Google Compute Engine, in most cases, you should omit the spring.cloud.gcp.credentials.location property and, instead, let the Spring Cloud GCP Starter get the correct credentials for those environments. +On App Engine Standard, the App Identity service account credentials are used, on App Engine Flexible, the Flexible service account credential are used and on Google Compute Engine, the Compute Engine Default Service Account is used.

    154.2.1 Scopes

    By default, the credentials provided by the Spring Cloud GCP Starter contain scopes for every service supported by Spring Cloud GCP.

    The Spring Cloud GCP starter allows you to configure a custom scope list for the provided credentials. +To do that, specify a comma-delimited list of Google OAuth2 scopes in the spring.cloud.gcp.credentials.scopes property.

    spring.cloud.gcp.credentials.scopes is a comma-delimited list of Google OAuth2 scopes for Google Cloud Platform services that the credentials returned by the provided CredentialsProvider support.

    spring.cloud.gcp.credentials.scopes=https://www.googleapis.com/auth/pubsub,https://www.googleapis.com/auth/sqlservice.admin

    You can also use DEFAULT_SCOPES placeholder as a scope to represent the starters default scopes, and append the additional scopes you need to add.

    spring.cloud.gcp.credentials.scopes=DEFAULT_SCOPES,https://www.googleapis.com/auth/cloud-vision

    154.3 Environment

    GcpEnvironmentProvider is a functional interface, auto-configured by the Spring Cloud GCP starter, that returns a GcpEnvironment enum. +The provider can help determine programmatically in which GCP environment (App Engine Flexible, App Engine Standard, Kubernetes Engine or Compute Engine) the application is deployed.

    public interface GcpEnvironmentProvider {
    +	GcpEnvironment getCurrentEnvironment();
    +}

    154.4 Spring Initializr

    This starter is available from Spring Initializr through the GCP Support entry.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-gcp-reference.html b/Greenwich.SR5/multi/multi_spring-cloud-gcp-reference.html new file mode 100644 index 00000000..27fc0cb1 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-gcp-reference.html @@ -0,0 +1,3 @@ + + + Part XVIII. Spring Cloud GCP

    Part XVIII. Spring Cloud GCP

    João André Martins; Jisha Abubaker; Ray Tsang; Mike Eltsufin; Artem Bilan; Andreas Berger; Balint Pato; Chengyuan Zhao; Dmitry Solomakha; Elena Felder; Daniel Zou

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-ribbon.html b/Greenwich.SR5/multi/multi_spring-cloud-ribbon.html new file mode 100644 index 00000000..ef53b7a0 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-ribbon.html @@ -0,0 +1,130 @@ + + + 16. Client Side Load Balancer: Ribbon

    16. Client Side Load Balancer: Ribbon

    Ribbon is a client-side load balancer that gives you a lot of control over the behavior of HTTP and TCP clients. +Feign already uses Ribbon, so, if you use @FeignClient, this section also applies.

    A central concept in Ribbon is that of the named client. +Each load balancer is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer (for example, by using the @FeignClient annotation). +On demand, Spring Cloud creates a new ensemble as an ApplicationContext for each named client by using +RibbonClientConfiguration. +This contains (amongst other things) an ILoadBalancer, a RestClient, and a ServerListFilter.

    16.1 How to Include Ribbon

    To include Ribbon in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-ribbon. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    16.2 Customizing the Ribbon Client

    You can configure some bits of a Ribbon client by using external properties in <client>.ribbon.*, which is similar to using the Netflix APIs natively, except that you can use Spring Boot configuration files. +The native options can be inspected as static fields in CommonClientConfigKey (part of ribbon-core).

    Spring Cloud also lets you take full control of the client by declaring additional configuration (on top of the RibbonClientConfiguration) using @RibbonClient, as shown in the following example:

    @Configuration
    +@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
    +public class TestConfiguration {
    +}

    In this case, the client is composed from the components already in RibbonClientConfiguration, together with any in CustomConfiguration (where the latter generally overrides the former).

    [Warning]Warning

    The CustomConfiguration clas must be a @Configuration class, but take care that it is not in a @ComponentScan for the main application context. +Otherwise, it is shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication), you need to take steps to avoid it being included (for instance, you can put it in a separate, non-overlapping package or specify the packages to scan explicitly in the @ComponentScan).

    The following table shows the beans that Spring Cloud Netflix provides by default for Ribbon:

    Bean TypeBean NameClass Name

    IClientConfig

    ribbonClientConfig

    DefaultClientConfigImpl

    IRule

    ribbonRule

    ZoneAvoidanceRule

    IPing

    ribbonPing

    DummyPing

    ServerList<Server>

    ribbonServerList

    ConfigurationBasedServerList

    ServerListFilter<Server>

    ribbonServerListFilter

    ZonePreferenceServerListFilter

    ILoadBalancer

    ribbonLoadBalancer

    ZoneAwareLoadBalancer

    ServerListUpdater

    ribbonServerListUpdater

    PollingServerListUpdater

    Creating a bean of one of those type and placing it in a @RibbonClient configuration (such as FooConfiguration above) lets you override each one of the beans described, as shown in the following example:

    @Configuration
    +protected static class FooConfiguration {
    +
    +	@Bean
    +	public ZonePreferenceServerListFilter serverListFilter() {
    +		ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
    +		filter.setZone("myTestZone");
    +		return filter;
    +	}
    +
    +	@Bean
    +	public IPing ribbonPing() {
    +		return new PingUrl();
    +	}
    +
    +}

    The include statement in the preceding example replaces NoOpPing with PingUrl and provides a custom serverListFilter.

    16.3 Customizing the Default for All Ribbon Clients

    A default configuration can be provided for all Ribbon Clients by using the @RibbonClients annotation and registering a default configuration, as shown in the following example:

    @RibbonClients(defaultConfiguration = DefaultRibbonConfig.class)
    +public class RibbonClientDefaultConfigurationTestsConfig {
    +
    +	public static class BazServiceList extends ConfigurationBasedServerList {
    +
    +		public BazServiceList(IClientConfig config) {
    +			super.initWithNiwsConfig(config);
    +		}
    +
    +	}
    +
    +}
    +
    +@Configuration
    +class DefaultRibbonConfig {
    +
    +	@Bean
    +	public IRule ribbonRule() {
    +		return new BestAvailableRule();
    +	}
    +
    +	@Bean
    +	public IPing ribbonPing() {
    +		return new PingUrl();
    +	}
    +
    +	@Bean
    +	public ServerList<Server> ribbonServerList(IClientConfig config) {
    +		return new RibbonClientDefaultConfigurationTestsConfig.BazServiceList(config);
    +	}
    +
    +	@Bean
    +	public ServerListSubsetFilter serverListFilter() {
    +		ServerListSubsetFilter filter = new ServerListSubsetFilter();
    +		return filter;
    +	}
    +
    +}

    16.4 Customizing the Ribbon Client by Setting Properties

    Starting with version 1.2.0, Spring Cloud Netflix now supports customizing Ribbon clients by setting properties to be compatible with the Ribbon documentation.

    This lets you change behavior at start up time in different environments.

    The following list shows the supported properties>:

    • <clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
    • <clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
    • <clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
    • <clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
    • <clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter
    [Note]Note

    Classes defined in these properties have precedence over beans defined by using @RibbonClient(configuration=MyRibbonConfig.class) and the defaults provided by Spring Cloud Netflix.

    To set the IRule for a service name called users, you could set the following properties:

    application.yml.  +

    users:
    +  ribbon:
    +    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    +    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

    +

    See the Ribbon documentation for implementations provided by Ribbon.

    16.5 Using Ribbon with Eureka

    When Eureka is used in conjunction with Ribbon (that is, both are on the classpath), the ribbonServerList is overridden with an extension of DiscoveryEnabledNIWSServerList, which populates the list of servers from Eureka. +It also replaces the IPing interface with NIWSDiscoveryPing, which delegates to Eureka to determine if a server is up. +The ServerList that is installed by default is a DomainExtractingServerList. Its purpose is to make metadata available to the load balancer without using AWS AMI metadata (which is what Netflix relies on). +By default, the server list is constructed with zone information, as provided in the instance metadata (so, on the remote clients, set eureka.instance.metadataMap.zone). +If that is missing and if the approximateZoneFromHostname flag is set, it can use the domain name from the server hostname as a proxy for the zone. +Once the zone information is available, it can be used in a ServerListFilter. +By default, it is used to locate a server in the same zone as the client, because the default is a ZonePreferenceServerListFilter. +By default, the zone of the client is determined in the same way as the remote instances (that is, through eureka.instance.metadataMap.zone).

    [Note]Note

    The orthodox archaius way to set the client zone is through a configuration property called "@zone". +If it is available, Spring Cloud uses that in preference to all other settings (note that the key must be quoted in YAML configuration).

    [Note]Note

    If there is no other source of zone data, then a guess is made, based on the client configuration (as opposed to the instance configuration). +We take eureka.client.availabilityZones, which is a map from region name to a list of zones, and pull out the first zone for the instance’s own region (that is, the eureka.client.region, which defaults to "us-east-1", for compatibility with native Netflix).

    16.6 Example: How to Use Ribbon Without Eureka

    Eureka is a convenient way to abstract the discovery of remote servers so that you do not have to hard code their URLs in clients. +However, if you prefer not to use Eureka, Ribbon and Feign also work. +Suppose you have declared a @RibbonClient for "stores", and Eureka is not in use (and not even on the classpath). +The Ribbon client defaults to a configured server list. +You can supply the configuration as follows:

    application.yml.  +

    stores:
    +  ribbon:
    +    listOfServers: example.com,google.com

    +

    16.7 Example: Disable Eureka Use in Ribbon

    Setting the ribbon.eureka.enabled property to false explicitly disables the use of Eureka in Ribbon, as shown in the following example:

    application.yml.  +

    ribbon:
    +  eureka:
    +   enabled: false

    +

    16.8 Using the Ribbon API Directly

    You can also use the LoadBalancerClient directly, as shown in the following example:

    public class MyClass {
    +    @Autowired
    +    private LoadBalancerClient loadBalancer;
    +
    +    public void doStuff() {
    +        ServiceInstance instance = loadBalancer.choose("stores");
    +        URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
    +        // ... do something with the URI
    +    }
    +}

    16.9 Caching of Ribbon Configuration

    Each Ribbon named client has a corresponding child application Context that Spring Cloud maintains. +This application context is lazily loaded on the first request to the named client. +This lazy loading behavior can be changed to instead eagerly load these child application contexts at startup, by specifying the names of the Ribbon clients, as shown in the following example:

    application.yml.  +

    ribbon:
    +  eager-load:
    +    enabled: true
    +    clients: client1, client2, client3

    +

    16.10 How to Configure Hystrix Thread Pools

    If you change zuul.ribbonIsolationStrategy to THREAD, the thread isolation strategy for Hystrix is used for all routes. +In that case, the HystrixThreadPoolKey is set to RibbonCommand as the default. +It means that HystrixCommands for all routes are executed in the same Hystrix thread pool. +This behavior can be changed with the following configuration:

    application.yml.  +

    zuul:
    +  threadPool:
    +    useSeparateThreadPools: true

    +

    The preceding example results in HystrixCommands being executed in the Hystrix thread pool for each route.

    In this case, the default HystrixThreadPoolKey is the same as the service ID for each route. +To add a prefix to HystrixThreadPoolKey, set zuul.threadPool.threadPoolKeyPrefix to the value that you want to add, as shown in the following example:

    application.yml.  +

    zuul:
    +  threadPool:
    +    useSeparateThreadPools: true
    +    threadPoolKeyPrefix: zuulgw

    +

    16.11 How to Provide a Key to Ribbon’s IRule

    If you need to provide your own IRule implementation to handle a special routing requirement like a canary test, pass some information to the choose method of IRule.

    com.netflix.loadbalancer.IRule.java.  +

    public interface IRule{
    +    public Server choose(Object key);
    +         :

    +

    You can provide some information that is used by your IRule implementation to choose a target server, as shown in the following example:

    RequestContext.getCurrentContext()
    +              .set(FilterConstants.LOAD_BALANCER_KEY, "canary-test");

    If you put any object into the RequestContext with a key of FilterConstants.LOAD_BALANCER_KEY, it is passed to the choose method of the IRule implementation. +The code shown in the preceding example must be executed before RibbonRoutingFilter is executed. +Zuul’s pre filter is the best place to do that. +You can access HTTP headers and query parameters through the RequestContext in pre filter, so it can be used to determine the LOAD_BALANCER_KEY that is passed to Ribbon. +If you do not put any value with LOAD_BALANCER_KEY in RequestContext, null is passed as a parameter of the choose method.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-binders.html b/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-binders.html new file mode 100644 index 00000000..5a79774f --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-binders.html @@ -0,0 +1,76 @@ + + + 30. Binders

    30. Binders

    Spring Cloud Stream provides a Binder abstraction for use in connecting to physical destinations at the external middleware. +This section provides information about the main concepts behind the Binder SPI, its main components, and implementation-specific details.

    30.1 Producers and Consumers

    The following image shows the general relationship of producers and consumers:

    Figure 30.1. Producers and Consumers

    producers consumers

    A producer is any component that sends messages to a channel. +The channel can be bound to an external message broker with a Binder implementation for that broker. +When invoking the bindProducer() method, the first parameter is the name of the destination within the broker, the second parameter is the local channel instance to which the producer sends messages, and the third parameter contains properties (such as a partition key expression) to be used within the adapter that is created for that channel.

    A consumer is any component that receives messages from a channel. +As with a producer, the consumer’s channel can be bound to an external message broker. +When invoking the bindConsumer() method, the first parameter is the destination name, and a second parameter provides the name of a logical group of consumers. +Each group that is represented by consumer bindings for a given destination receives a copy of each message that a producer sends to that destination (that is, it follows normal publish-subscribe semantics). +If there are multiple consumer instances bound with the same group name, then messages are load-balanced across those consumer instances so that each message sent by a producer is consumed by only a single consumer instance within each group (that is, it follows normal queueing semantics).

    30.2 Binder SPI

    The Binder SPI consists of a number of interfaces, out-of-the box utility classes, and discovery strategies that provide a pluggable mechanism for connecting to external middleware.

    The key point of the SPI is the Binder interface, which is a strategy for connecting inputs and outputs to external middleware. The following listing shows the definnition of the Binder interface:

    public interface Binder<T, C extends ConsumerProperties, P extends ProducerProperties> {
    +    Binding<T> bindConsumer(String name, String group, T inboundBindTarget, C consumerProperties);
    +
    +    Binding<T> bindProducer(String name, T outboundBindTarget, P producerProperties);
    +}

    The interface is parameterized, offering a number of extension points:

    • Input and output bind targets. As of version 1.0, only MessageChannel is supported, but this is intended to be used as an extension point in the future.
    • Extended consumer and producer properties, allowing specific Binder implementations to add supplemental properties that can be supported in a type-safe manner.

    A typical binder implementation consists of the following:

    • A class that implements the Binder interface;
    • A Spring @Configuration class that creates a bean of type Binder along with the middleware connection infrastructure.
    • A META-INF/spring.binders file found on the classpath containing one or more binder definitions, as shown in the following example:

      kafka:\
      +org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration

    30.3 Binder Detection

    Spring Cloud Stream relies on implementations of the Binder SPI to perform the task of connecting channels to message brokers. +Each Binder implementation typically connects to one type of messaging system.

    30.3.1 Classpath Detection

    By default, Spring Cloud Stream relies on Spring Boot’s auto-configuration to configure the binding process. +If a single Binder implementation is found on the classpath, Spring Cloud Stream automatically uses it. +For example, a Spring Cloud Stream project that aims to bind only to RabbitMQ can add the following dependency:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    +</dependency>

    For the specific Maven coordinates of other binder dependencies, see the documentation of that binder implementation.

    30.4 Multiple Binders on the Classpath

    When multiple binders are present on the classpath, the application must indicate which binder is to be used for each channel binding. +Each binder configuration contains a META-INF/spring.binders file, which is a simple properties file, as shown in the following example:

    rabbit:\
    +org.springframework.cloud.stream.binder.rabbit.config.RabbitServiceAutoConfiguration

    Similar files exist for the other provided binder implementations (such as Kafka), and custom binder implementations are expected to provide them as well. +The key represents an identifying name for the binder implementation, whereas the value is a comma-separated list of configuration classes that each contain one and only one bean definition of type org.springframework.cloud.stream.binder.Binder.

    Binder selection can either be performed globally, using the spring.cloud.stream.defaultBinder property (for example, spring.cloud.stream.defaultBinder=rabbit) or individually, by configuring the binder on each channel binding. +For instance, a processor application (that has channels named input and output for read and write respectively) that reads from Kafka and writes to RabbitMQ can specify the following configuration:

    spring.cloud.stream.bindings.input.binder=kafka
    +spring.cloud.stream.bindings.output.binder=rabbit

    30.5 Connecting to Multiple Systems

    By default, binders share the application’s Spring Boot auto-configuration, so that one instance of each binder found on the classpath is created. +If your application should connect to more than one broker of the same type, you can specify multiple binder configurations, each with different environment settings.

    [Note]Note

    Turning on explicit binder configuration disables the default binder configuration process altogether. +If you do so, all binders in use must be included in the configuration. +Frameworks that intend to use Spring Cloud Stream transparently may create binder configurations that can be referenced by name, but they do not affect the default binder configuration. +In order to do so, a binder configuration may have its defaultCandidate flag set to false (for example, spring.cloud.stream.binders.<configurationName>.defaultCandidate=false). +This denotes a configuration that exists independently of the default binder configuration process.

    The following example shows a typical configuration for a processor application that connects to two RabbitMQ broker instances:

    spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        input:
    +          destination: thing1
    +          binder: rabbit1
    +        output:
    +          destination: thing2
    +          binder: rabbit2
    +      binders:
    +        rabbit1:
    +          type: rabbit
    +          environment:
    +            spring:
    +              rabbitmq:
    +                host: <host1>
    +        rabbit2:
    +          type: rabbit
    +          environment:
    +            spring:
    +              rabbitmq:
    +                host: <host2>

    30.6 Binding visualization and control

    Since version 2.0, Spring Cloud Stream supports visualization and control of the Bindings through Actuator endpoints.

    Starting with version 2.0 actuator and web are optional, you must first add one of the web dependencies as well as add the actuator dependency manually. +The following example shows how to add the dependency for the Web framework:

    <dependency>
    +     <groupId>org.springframework.boot</groupId>
    +     <artifactId>spring-boot-starter-web</artifactId>
    +</dependency>

    The following example shows how to add the dependency for the WebFlux framework:

    <dependency>
    +       <groupId>org.springframework.boot</groupId>
    +       <artifactId>spring-boot-starter-webflux</artifactId>
    +</dependency>

    You can add the Actuator dependency as follows:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-actuator</artifactId>
    +</dependency>
    [Note]Note

    To run Spring Cloud Stream 2.0 apps in Cloud Foundry, you must add spring-boot-starter-web and spring-boot-starter-actuator to the classpath. Otherwise, the +application will not start due to health check failures.

    You must also enable the bindings actuator endpoints by setting the following property: --management.endpoints.web.exposure.include=bindings.

    Once those prerequisites are satisfied. you should see the following in the logs when application start:

    : Mapped "{[/actuator/bindings/{name}],methods=[POST]. . .
    +: Mapped "{[/actuator/bindings],methods=[GET]. . .
    +: Mapped "{[/actuator/bindings/{name}],methods=[GET]. . .

    To visualize the current bindings, access the following URL: +http://<host>:<port>/actuator/bindings

    Alternative, to see a single binding, access one of the URLs similar to the following: +http://<host>:<port>/actuator/bindings/myBindingName

    You can also stop, start, pause, and resume individual bindings by posting to the same URL while providing a state argument as JSON, as shown in the following examples:

    curl -d '{"state":"STOPPED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"STARTED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"PAUSED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"RESUMED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName

    [Note]Note

    PAUSED and RESUMED work only when the corresponding binder and its underlying technology supports it. Otherwise, you see the warning message in the logs. +Currently, only Kafka binder supports the PAUSED and RESUMED states.

    30.7 Binder Configuration Properties

    The following properties are available when customizing binder configurations. These properties exposed via org.springframework.cloud.stream.config.BinderProperties

    They must be prefixed with spring.cloud.stream.binders.<configurationName>.

    type

    The binder type. +It typically references one of the binders found on the classpath — in particular, a key in a META-INF/spring.binders file.

    By default, it has the same value as the configuration name.

    inheritEnvironment

    Whether the configuration inherits the environment of the application itself.

    Default: true.

    environment

    Root for a set of properties that can be used to customize the environment of the binder. +When this property is set, the context in which the binder is being created is not a child of the application context. +This setting allows for complete separation between the binder components and the application components.

    Default: empty.

    defaultCandidate

    Whether the binder configuration is a candidate for being considered a default binder or can be used only when explicitly referenced. +This setting allows adding binder configurations without interfering with the default processing.

    Default: true.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-introducing.html b/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-introducing.html new file mode 100644 index 00000000..a0a4ad1a --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-introducing.html @@ -0,0 +1,42 @@ + + + 27. Introducing Spring Cloud Stream

    27. Introducing Spring Cloud Stream

    Spring Cloud Stream is a framework for building message-driven microservice applications. +Spring Cloud Stream builds upon Spring Boot to create standalone, production-grade Spring applications and uses Spring Integration to provide connectivity to message brokers. +It provides opinionated configuration of middleware from several vendors, introducing the concepts of persistent publish-subscribe semantics, consumer groups, and partitions.

    You can add the @EnableBinding annotation to your application to get immediate connectivity to a message broker, and you can add @StreamListener to a method to cause it to receive events for stream processing. +The following example shows a sink application that receives external messages:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class VoteRecordingSinkApplication {
    +
    +  public static void main(String[] args) {
    +    SpringApplication.run(VoteRecordingSinkApplication.class, args);
    +  }
    +
    +  @StreamListener(Sink.INPUT)
    +  public void processVote(Vote vote) {
    +      votingService.recordVote(vote);
    +  }
    +}

    The @EnableBinding annotation takes one or more interfaces as parameters (in this case, the parameter is a single Sink interface). +An interface declares input and output channels. +Spring Cloud Stream provides the Source, Sink, and Processor interfaces. You can also define your own interfaces.

    The following listing shows the definition of the Sink interface:

    public interface Sink {
    +  String INPUT = "input";
    +
    +  @Input(Sink.INPUT)
    +  SubscribableChannel input();
    +}

    The @Input annotation identifies an input channel, through which received messages enter the application. +The @Output annotation identifies an output channel, through which published messages leave the application. +The @Input and @Output annotations can take a channel name as a parameter. +If a name is not provided, the name of the annotated method is used.

    Spring Cloud Stream creates an implementation of the interface for you. +You can use this in the application by autowiring it, as shown in the following example (from a test case):

    @RunWith(SpringJUnit4ClassRunner.class)
    +@SpringApplicationConfiguration(classes = VoteRecordingSinkApplication.class)
    +@WebAppConfiguration
    +@DirtiesContext
    +public class StreamApplicationTests {
    +
    +  @Autowired
    +  private Sink sink;
    +
    +  @Test
    +  public void contextLoads() {
    +    assertNotNull(this.sink.input());
    +  }
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-metrics-emitter.html b/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-metrics-emitter.html new file mode 100644 index 00000000..525c31eb --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-stream-overview-metrics-emitter.html @@ -0,0 +1,50 @@ + + + 37. Metrics Emitter

    37. Metrics Emitter

    Spring Boot Actuator provides dependency management and auto-configuration for Micrometer, an application metrics +facade that supports numerous monitoring systems.

    Spring Cloud Stream provides support for emitting any available micrometer-based metrics to a binding destination, allowing for periodic +collection of metric data from stream applications without relying on polling individual endpoints.

    Metrics Emitter is activated by defining the spring.cloud.stream.bindings.applicationMetrics.destination property, +which specifies the name of the binding destination used by the current binder to publish metric messages.

    For example:

    spring.cloud.stream.bindings.applicationMetrics.destination=myMetricDestination

    The preceding example instructs the binder to bind to myMetricDestination (that is, Rabbit exchange, Kafka topic, and others).

    The following properties can be used for customizing the emission of metrics:

    spring.cloud.stream.metrics.key

    The name of the metric being emitted. Should be a unique value per application.

    Default: ${spring.application.name:${vcap.application.name:${spring.config.name:application}}}

    spring.cloud.stream.metrics.properties

    Allows white listing application properties that are added to the metrics payload

    Default: null.

    spring.cloud.stream.metrics.meter-filter

    Pattern to control the 'meters' one wants to capture. +For example, specifying spring.integration.* captures metric information for meters whose name starts with spring.integration.

    Default: all 'meters' are captured.

    spring.cloud.stream.metrics.schedule-interval

    Interval to control the rate of publishing metric data.

    Default: 1 min

    Consider the following:

    java -jar time-source.jar \
    +    --spring.cloud.stream.bindings.applicationMetrics.destination=someMetrics \
    +    --spring.cloud.stream.metrics.properties=spring.application** \
    +    --spring.cloud.stream.metrics.meter-filter=spring.integration.*

    The following example shows the payload of the data published to the binding destination as a result of the preceding command:

    {
    +	"name": "application",
    +	"createdTime": "2018-03-23T14:48:12.700Z",
    +	"properties": {
    +	},
    +	"metrics": [
    +		{
    +			"id": {
    +				"name": "spring.integration.send",
    +				"tags": [
    +					{
    +						"key": "exception",
    +						"value": "none"
    +					},
    +					{
    +						"key": "name",
    +						"value": "input"
    +					},
    +					{
    +						"key": "result",
    +						"value": "success"
    +					},
    +					{
    +						"key": "type",
    +						"value": "channel"
    +					}
    +				],
    +				"type": "TIMER",
    +				"description": "Send processing time",
    +				"baseUnit": "milliseconds"
    +			},
    +			"timestamp": "2018-03-23T14:48:12.697Z",
    +			"sum": 130.340546,
    +			"count": 6,
    +			"mean": 21.72342433333333,
    +			"upper": 116.176299,
    +			"total": 130.340546
    +		}
    +	]
    +}
    [Note]Note

    Given that the format of the Metric message has slightly changed after migrating to Micrometer, the published message will also have +a STREAM_CLOUD_STREAM_VERSION header set to 2.x to help distinguish between Metric messages from the older versions of the Spring Cloud Stream.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-config.html b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-config.html new file mode 100644 index 00000000..462fe534 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-config.html @@ -0,0 +1,61 @@ + + + 80. Distributed Configuration with Zookeeper

    80. Distributed Configuration with Zookeeper

    Zookeeper provides a +hierarchical namespace +that lets clients store arbitrary data, such as configuration data. Spring Cloud Zookeeper +Config is an alternative to the +Config Server and Client. +Configuration is loaded into the Spring Environment during the special bootstrap +phase. Configuration is stored in the /config namespace by default. Multiple +PropertySource instances are created, based on the application’s name and the active +profiles, to mimic the Spring Cloud Config order of resolving properties. For example, an +application with a name of testApp and with the dev profile has the following property +sources created for it:

    • config/testApp,dev
    • config/testApp
    • config/application,dev
    • config/application

    The most specific property source is at the top, with the least specific at the bottom. +Properties in the config/application namespace apply to all applications that use +zookeeper for configuration. Properties in the config/testApp namespace are available +only to the instances of the service named testApp.

    Configuration is currently read on startup of the application. Sending a HTTP POST +request to /refresh causes the configuration to be reloaded. Watching the configuration +namespace (which Zookeeper supports) is not currently implemented.

    80.1 Activating

    Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-config enables +autoconfiguration that sets up Spring Cloud Zookeeper Config.

    [Caution]Caution

    When working with version 3.4 of Zookeeper you need to change +the way you include the dependency as described here.

    80.2 Customizing

    Zookeeper Config may be customized by setting the following properties:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    zookeeper:
    +      config:
    +        enabled: true
    +        root: configuration
    +        defaultContext: apps
    +        profileSeparator: '::'

    +

    • enabled: Setting this value to false disables Zookeeper Config.
    • root: Sets the base namespace for configuration values.
    • defaultContext: Sets the name used by all applications.
    • profileSeparator: Sets the value of the separator used to separate the profile name in +property sources with profiles.

    80.3 Access Control Lists (ACLs)

    You can add authentication information for Zookeeper ACLs by calling the addAuthInfo +method of a CuratorFramework bean. One way to accomplish this is to provide your own +CuratorFramework bean, as shown in the following example:

    @BoostrapConfiguration
    +public class CustomCuratorFrameworkConfig {
    +
    +  @Bean
    +  public CuratorFramework curatorFramework() {
    +    CuratorFramework curator = new CuratorFramework();
    +    curator.addAuthInfo("digest", "user:password".getBytes());
    +    return curator;
    +  }
    +
    +}

    Consult +the ZookeeperAutoConfiguration class +to see how the CuratorFramework bean’s default configuration.

    Alternatively, you can add your credentials from a class that depends on the existing +CuratorFramework bean, as shown in the following example:

    @BoostrapConfiguration
    +public class DefaultCuratorFrameworkConfig {
    +
    +  public ZookeeperConfig(CuratorFramework curator) {
    +    curator.addAuthInfo("digest", "user:password".getBytes());
    +  }
    +
    +}

    The creation of this bean must occur during the boostrapping phase. You can register +configuration classes to run during this phase by annotating them with +@BootstrapConfiguration and including them in a comma-separated list that you set as the +value of the org.springframework.cloud.bootstrap.BootstrapConfiguration property in the +resources/META-INF/spring.factories file, as shown in the following example:

    resources/META-INF/spring.factories.  +

    org.springframework.cloud.bootstrap.BootstrapConfiguration=\
    +my.project.CustomCuratorFrameworkConfig,\
    +my.project.DefaultCuratorFrameworkConfig

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependencies.html b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependencies.html new file mode 100644 index 00000000..37f6447b --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependencies.html @@ -0,0 +1,86 @@ + + + 78. Zookeeper Dependencies

    78. Zookeeper Dependencies

    The following topics cover how to work with Spring Cloud Zookeeper dependencies:

    78.1 Using the Zookeeper Dependencies

    Spring Cloud Zookeeper gives you a possibility to provide dependencies of your application +as properties. As dependencies, you can understand other applications that are registered +in Zookeeper and which you would like to call through +Feign +(a REST client builder) and Spring RestTemplate.

    You can also use the Zookeeper Dependency Watchers functionality to control and monitor +the state of your dependencies.

    78.2 Activating Zookeeper Dependencies

    Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-discovery enables +autoconfiguration that sets up Spring Cloud Zookeeper Dependencies. Even if you provide +the dependencies in your properties, you can turn off the dependencies. To do so, set the +spring.cloud.zookeeper.dependency.enabled property to false (it defaults to true).

    78.3 Setting up Zookeeper Dependencies

    Consider the following example of dependency representation:

    application.yml.  +

    spring.application.name: yourServiceName
    +spring.cloud.zookeeper:
    +  dependencies:
    +    newsletter:
    +      path: /path/where/newsletter/has/registered/in/zookeeper
    +      loadBalancerType: ROUND_ROBIN
    +      contentTypeTemplate: application/vnd.newsletter.$version+json
    +      version: v1
    +      headers:
    +        header1:
    +            - value1
    +        header2:
    +            - value2
    +      required: false
    +      stubs: org.springframework:foo:stubs
    +    mailing:
    +      path: /path/where/mailing/has/registered/in/zookeeper
    +      loadBalancerType: ROUND_ROBIN
    +      contentTypeTemplate: application/vnd.mailing.$version+json
    +      version: v1
    +      required: true

    +

    The next few sections go through each part of the dependency one by one. The root property +name is spring.cloud.zookeeper.dependencies.

    78.3.1 Aliases

    Below the root property you have to represent each dependency as an alias. This is due to +the constraints of Ribbon, which requires that the application ID be placed in the URL. +Consequently, you cannot pass any complex path, suchas /myApp/myRoute/name). The alias +is the name you use instead of the serviceId for DiscoveryClient, Feign, or +RestTemplate.

    In the previous examples, the aliases are newsletter and mailing. The following +example shows Feign usage with a newsletter alias:

    @FeignClient("newsletter")
    +public interface NewsletterService {
    +        @RequestMapping(method = RequestMethod.GET, value = "/newsletter")
    +        String getNewsletters();
    +}

    78.3.2 Path

    The path is represented by the path YAML property and is the path under which the +dependency is registered under Zookeeper. As described in the +previous section, Ribbon +operates on URLs. As a result, this path is not compliant with its requirement. +That is why Spring Cloud Zookeeper maps the alias to the proper path.

    78.3.3 Load Balancer Type

    The load balancer type is represented by loadBalancerType YAML property.

    If you know what kind of load-balancing strategy has to be applied when calling this +particular dependency, you can provide it in the YAML file, and it is automatically +applied. You can choose one of the following load balancing strategies:

    • STICKY: Once chosen, the instance is always called.
    • RANDOM: Picks an instance randomly.
    • ROUND_ROBIN: Iterates over instances over and over again.

    78.3.4 Content-Type Template and Version

    The Content-Type template and version are represented by the contentTypeTemplate and +version YAML properties.

    If you version your API in the Content-Type header, you do not want to add this header +to each of your requests. Also, if you want to call a new version of the API, you do not +want to roam around your code to bump up the API version. That is why you can provide a +contentTypeTemplate with a special $version placeholder. That placeholder will be filled by the value of the +version YAML property. Consider the following example of a contentTypeTemplate:

    application/vnd.newsletter.$version+json

    Further consider the following version:

    v1

    The combination of contentTypeTemplate and version results in the creation of a +Content-Type header for each request, as follows:

    application/vnd.newsletter.v1+json

    78.3.5 Default Headers

    Default headers are represented by the headers map in YAML.

    Sometimes, each call to a dependency requires setting up of some default headers. To not +do that in code, you can set them up in the YAML file, as shown in the following example +headers section:

    headers:
    +    Accept:
    +        - text/html
    +        - application/xhtml+xml
    +    Cache-Control:
    +        - no-cache

    That headers section results in adding the Accept and Cache-Control headers with +appropriate list of values in your HTTP request.

    78.3.6 Required Dependencies

    Required dependencies are represented by required property in YAML.

    If one of your dependencies is required to be up when your application boots, you can set +the required: true property in the YAML file.

    If your application cannot localize the required dependency during boot time, it throws an +exception, and the Spring Context fails to set up. In other words, your application cannot +start if the required dependency is not registered in Zookeeper.

    You can read more about Spring Cloud Zookeeper Presence Checker +later in this document.

    78.3.7 Stubs

    You can provide a colon-separated path to the JAR containing stubs of the dependency, as +shown in the following example:

    stubs: org.springframework:myApp:stubs

    where:

    • org.springframework is the groupId.
    • myApp is the artifactId.
    • stubs is the classifier. (Note that stubs is the default value.)

    Because stubs is the default classifier, the preceding example is equal to the following +example:

    stubs: org.springframework:myApp

    78.4 Configuring Spring Cloud Zookeeper Dependencies

    You can set the following properties to enable or disable parts of Zookeeper Dependencies +functionalities:

    • spring.cloud.zookeeper.dependencies: If you do not set this property, you cannot use +Zookeeper Dependencies.
    • spring.cloud.zookeeper.dependency.ribbon.enabled (enabled by default): Ribbon requires +either explicit global configuration or a particular one for a dependency. By turning on +this property, runtime load balancing strategy resolution is possible, and you can use the +loadBalancerType section of the Zookeeper Dependencies. The configuration that needs +this property has an implementation of LoadBalancerClient that delegates to the +ILoadBalancer presented in the next bullet.
    • spring.cloud.zookeeper.dependency.ribbon.loadbalancer (enabled by default): Thanks to +this property, the custom ILoadBalancer knows that the part of the URI passed to Ribbon +might actually be the alias that has to be resolved to a proper path in Zookeeper. Without +this property, you cannot register applications under nested paths.
    • spring.cloud.zookeeper.dependency.headers.enabled (enabled by default): This property +registers a RibbonClient that automatically appends appropriate headers and content +types with their versions, as presented in the Dependency configuration. Without this +setting, those two parameters do not work.
    • spring.cloud.zookeeper.dependency.resttemplate.enabled (enabled by default): When +enabled, this property modifies the request headers of a @LoadBalanced-annotated +RestTemplate such that it passes headers and content type with the version set in +dependency configuration. Without this setting, those two parameters do not work.
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependency-watcher.html b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependency-watcher.html new file mode 100644 index 00000000..8646c32d --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-dependency-watcher.html @@ -0,0 +1,21 @@ + + + 79. Spring Cloud Zookeeper Dependency Watcher

    79. Spring Cloud Zookeeper Dependency Watcher

    The Dependency Watcher mechanism lets you register listeners to your dependencies. The +functionality is, in fact, an implementation of the Observator pattern. When a +dependency changes, its state (to either UP or DOWN), some custom logic can be applied.

    79.1 Activating

    Spring Cloud Zookeeper Dependencies functionality needs to be enabled for you to use the +Dependency Watcher mechanism.

    79.2 Registering a Listener

    To register a listener, you must implement an interface called +org.springframework.cloud.zookeeper.discovery.watcher.DependencyWatcherListener and +register it as a bean. The interface gives you one method:

    void stateChanged(String dependencyName, DependencyState newState);

    If you want to register a listener for a particular dependency, the dependencyName would +be the discriminator for your concrete implementation. newState provides you with +information about whether your dependency has changed to CONNECTED or DISCONNECTED.

    79.3 Using the Presence Checker

    Bound with the Dependency Watcher is the functionality called Presence Checker. It lets +you provide custom behavior when your application boots, to react according to the state +of your dependencies.

    The default implementation of the abstract +org.springframework.cloud.zookeeper.discovery.watcher.presence.DependencyPresenceOnStartupVerifier +class is the +org.springframework.cloud.zookeeper.discovery.watcher.presence.DefaultDependencyPresenceOnStartupVerifier, +which works in the following way.

    1. If the dependency is marked us required and is not in Zookeeper, when your application +boots, it throws an exception and shuts down.
    2. If the dependency is not required, the +org.springframework.cloud.zookeeper.discovery.watcher.presence.LogMissingDependencyChecker +logs that the dependency is missing at the WARN level.

    Because the DefaultDependencyPresenceOnStartupVerifier is registered only when there is +no bean of type DependencyPresenceOnStartupVerifier, this functionality can be +overridden.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-discovery.html b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-discovery.html new file mode 100644 index 00000000..964cbff8 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-discovery.html @@ -0,0 +1,53 @@ + + + 75. Service Discovery with Zookeeper

    75. Service Discovery with Zookeeper

    Service Discovery is one of the key tenets of a microservice based architecture. Trying to +hand-configure each client or some form of convention can be difficult to do and can be +brittle. Curator(A Java library for Zookeeper) provides Service +Discovery through a Service Discovery +Extension. Spring Cloud Zookeeper uses this extension for service registration and +discovery.

    75.1 Activating

    Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-discovery enables +autoconfiguration that sets up Spring Cloud Zookeeper Discovery.

    [Note]Note

    For web functionality, you still need to include +org.springframework.boot:spring-boot-starter-web.

    [Caution]Caution

    When working with version 3.4 of Zookeeper you need to change +the way you include the dependency as described here.

    75.2 Registering with Zookeeper

    When a client registers with Zookeeper, it provides metadata (such as host and port, ID, +and name) about itself.

    The following example shows a Zookeeper client:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello world";
    +    }
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}
    [Note]Note

    The preceding example is a normal Spring Boot application.

    If Zookeeper is located somewhere other than localhost:2181, the configuration must +provide the location of the server, as shown in the following example:

    application.yml.  +

    spring:
    +  cloud:
    +    zookeeper:
    +      connect-string: localhost:2181

    +

    [Caution]Caution

    If you use Spring Cloud Zookeeper Config, the +values shown in the preceding example need to be in bootstrap.yml instead of +application.yml.

    The default service name, instance ID, and port (taken from the Environment) are +${spring.application.name}, the Spring Context ID, and ${server.port}, respectively.

    Having spring-cloud-starter-zookeeper-discovery on the classpath makes the app into both +a Zookeeper service (that is, it registers itself) and a client (that is, it can +query Zookeeper to locate other services).

    If you would like to disable the Zookeeper Discovery Client, you can set +spring.cloud.zookeeper.discovery.enabled to false.

    75.3 Using the DiscoveryClient

    Spring Cloud has support for +Feign +(a REST client builder) and +Spring +RestTemplate, using logical service names instead of physical URLs.

    You can also use the org.springframework.cloud.client.discovery.DiscoveryClient, which +provides a simple API for discovery clients that is not specific to Netflix, as shown in +the following example:

    @Autowired
    +private DiscoveryClient discoveryClient;
    +
    +public String serviceUrl() {
    +    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    +    if (list != null && list.size() > 0 ) {
    +        return list.get(0).getUri().toString();
    +    }
    +    return null;
    +}
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-install.html b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-install.html new file mode 100644 index 00000000..15affd08 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-install.html @@ -0,0 +1,40 @@ + + + 74. Install Zookeeper

    74. Install Zookeeper

    See the installation +documentation for instructions on how to install Zookeeper.

    Spring Cloud Zookeeper uses Apache Curator behind the scenes. +While Zookeeper 3.5.x is still considered "beta" by the Zookeeper development team, +the reality is that it is used in production by many users. +However, Zookeeper 3.4.x is also used in production. +Prior to Apache Curator 4.0, both versions of Zookeeper were supported via two versions of Apache Curator. +Starting with Curator 4.0 both versions of Zookeeper are supported via the same Curator libraries.

    In case you are integrating with version 3.4 you need to change the Zookeeper dependency +that comes shipped with curator, and thus spring-cloud-zookeeper. +To do so simply exclude that dependency and add the 3.4.x version like shown below.

    maven.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-zookeeper-all</artifactId>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>org.apache.zookeeper</groupId>
    +            <artifactId>zookeeper</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>
    +<dependency>
    +    <groupId>org.apache.zookeeper</groupId>
    +    <artifactId>zookeeper</artifactId>
    +    <version>3.4.12</version>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>org.slf4j</groupId>
    +            <artifactId>slf4j-log4j12</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>

    +

    gradle.  +

    compile('org.springframework.cloud:spring-cloud-starter-zookeeper-all') {
    +  exclude group: 'org.apache.zookeeper', module: 'zookeeper'
    +}
    +compile('org.apache.zookeeper:zookeeper:3.4.12') {
    +  exclude group: 'org.slf4j', module: 'slf4j-log4j12'
    +}

    +

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-netflix.html b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-netflix.html new file mode 100644 index 00000000..cfd1fe80 --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-netflix.html @@ -0,0 +1,7 @@ + + + 76. Using Spring Cloud Zookeeper with Spring Cloud Netflix Components

    76. Using Spring Cloud Zookeeper with Spring Cloud Netflix Components

    Spring Cloud Netflix supplies useful tools that work regardless of which DiscoveryClient +implementation you use. Feign, Turbine, Ribbon, and Zuul all work with Spring Cloud +Zookeeper.

    76.1 Ribbon with Zookeeper

    Spring Cloud Zookeeper provides an implementation of Ribbon’s ServerList. When you use +the spring-cloud-starter-zookeeper-discovery, Ribbon is autoconfigured to use the +ZookeeperServerList by default.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-service-registry.html b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-service-registry.html new file mode 100644 index 00000000..69c90d4b --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud-zookeeper-service-registry.html @@ -0,0 +1,26 @@ + + + 77. Spring Cloud Zookeeper and Service Registry

    77. Spring Cloud Zookeeper and Service Registry

    Spring Cloud Zookeeper implements the ServiceRegistry interface, letting developers +register arbitrary services in a programmatic way.

    The ServiceInstanceRegistration class offers a builder() method to create a +Registration object that can be used by the ServiceRegistry, as shown in the following +example:

    @Autowired
    +private ZookeeperServiceRegistry serviceRegistry;
    +
    +public void registerThings() {
    +    ZookeeperRegistration registration = ServiceInstanceRegistration.builder()
    +            .defaultUriSpec()
    +            .address("anyUrl")
    +            .port(10)
    +            .name("/a/b/c/d/anotherservice")
    +            .build();
    +    this.serviceRegistry.register(registration);
    +}

    77.1 Instance Status

    Netflix Eureka supports having instances that are OUT_OF_SERVICE registered with the +server. These instances are not returned as active service instances. This is useful for +behaviors such as blue/green deployments. (Note that the Curator Service Discovery recipe +does not support this behavior.) Taking advantage of the flexible payload has let Spring +Cloud Zookeeper implement OUT_OF_SERVICE by updating some specific metadata and then +filtering on that metadata in the Ribbon ZookeeperServerList. The ZookeeperServerList +filters out all non-null instance statuses that do not equal UP. If the instance status +field is empty, it is considered to be UP for backwards compatibility. To change the +status of an instance, make a POST with OUT_OF_SERVICE to the ServiceRegistry +instance status actuator endpoint, as shown in the following example:

    $ http POST http://localhost:8081/service-registry status=OUT_OF_SERVICE
    [Note]Note

    The preceding example uses the http command from https://httpie.org.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_spring-cloud.html b/Greenwich.SR5/multi/multi_spring-cloud.html new file mode 100644 index 00000000..ce98412c --- /dev/null +++ b/Greenwich.SR5/multi/multi_spring-cloud.html @@ -0,0 +1,3 @@ + + + Spring Cloud

    Spring Cloud


    Table of Contents

    1. Features
    I. Cloud Native Applications
    2. Spring Cloud Context: Application Context Services
    2.1. The Bootstrap Application Context
    2.2. Application Context Hierarchies
    2.3. Changing the Location of Bootstrap Properties
    2.4. Overriding the Values of Remote Properties
    2.5. Customizing the Bootstrap Configuration
    2.6. Customizing the Bootstrap Property Sources
    2.7. Logging Configuration
    2.8. Environment Changes
    2.9. Refresh Scope
    2.10. Encryption and Decryption
    2.11. Endpoints
    3. Spring Cloud Commons: Common Abstractions
    3.1. @EnableDiscoveryClient
    3.1.1. Health Indicator
    3.1.2. Ordering DiscoveryClient instances
    3.2. ServiceRegistry
    3.2.1. ServiceRegistry Auto-Registration
    ServiceRegistry Auto-Registration Events
    3.2.2. Service Registry Actuator Endpoint
    3.3. Spring RestTemplate as a Load Balancer Client
    3.4. Spring WebClient as a Load Balancer Client
    3.4.1. Retrying Failed Requests
    3.5. Multiple RestTemplate objects
    3.6. Multiple WebClient Objects
    3.7. Spring WebFlux WebClient as a Load Balancer Client
    3.7.1. Spring WebFlux WebClient with Reactive Load Balancer
    3.7.2. Spring WebFlux WebClient with non-reactive Load Balancer Client
    3.7.3. Passing your own Load-Balancer Client configuration
    3.8. Ignore Network Interfaces
    3.9. HTTP Client Factories
    3.10. Enabled Features
    3.10.1. Feature types
    3.10.2. Declaring features
    3.11. Spring Cloud Compatibility Verification
    II. Spring Cloud Config
    4. Quick Start
    4.1. Client Side Usage
    5. Spring Cloud Config Server
    5.1. Environment Repository
    5.1.1. Git Backend
    Skipping SSL Certificate Validation
    Setting HTTP Connection Timeout
    Placeholders in Git URI
    Pattern Matching and Multiple Repositories
    Authentication
    Authentication with AWS CodeCommit
    Git SSH configuration using properties
    Placeholders in Git Search Paths
    Force pull in Git Repositories
    Deleting untracked branches in Git Repositories
    Git Refresh Rate
    5.1.2. Version Control Backend Filesystem Use
    5.1.3. File System Backend
    5.1.4. Vault Backend
    Multiple Properties Sources
    5.1.5. Accessing Backends Through a Proxy
    5.1.6. Sharing Configuration With All Applications
    File Based Repositories
    Vault Server
    CredHub Server
    5.1.7. JDBC Backend
    5.1.8. CredHub Backend
    OAuth 2.0
    5.1.9. Composite Environment Repositories
    Custom Composite Environment Repositories
    5.1.10. Property Overrides
    5.2. Health Indicator
    5.3. Security
    5.4. Encryption and Decryption
    5.5. Key Management
    5.6. Creating a Key Store for Testing
    5.7. Using Multiple Keys and Key Rotation
    5.8. Serving Encrypted Properties
    6. Serving Alternative Formats
    7. Serving Plain Text
    8. Embedding the Config Server
    9. Push Notifications and Spring Cloud Bus
    10. Spring Cloud Config Client
    10.1. Config First Bootstrap
    10.2. Discovery First Bootstrap
    10.3. Config Client Fail Fast
    10.4. Config Client Retry
    10.5. Locating Remote Configuration Resources
    10.6. Specifying Multiple Urls for the Config Server
    10.7. Configuring Timeouts
    10.8. Security
    10.8.1. Health Indicator
    10.8.2. Providing A Custom RestTemplate
    10.8.3. Vault
    10.9. Nested Keys In Vault
    III. Spring Cloud Netflix
    11. Service Discovery: Eureka Clients
    11.1. How to Include Eureka Client
    11.2. Registering with Eureka
    11.3. Authenticating with the Eureka Server
    11.4. Status Page and Health Indicator
    11.5. Registering a Secure Application
    11.6. Eureka’s Health Checks
    11.7. Eureka Metadata for Instances and Clients
    11.7.1. Using Eureka on Cloud Foundry
    11.7.2. Using Eureka on AWS
    11.7.3. Changing the Eureka Instance ID
    11.8. Using the EurekaClient
    11.8.1. EurekaClient without Jersey
    11.9. Alternatives to the Native Netflix EurekaClient
    11.10. Why Is It so Slow to Register a Service?
    11.11. Zones
    11.12. Refreshing Eureka Clients
    12. Service Discovery: Eureka Server
    12.1. How to Include Eureka Server
    12.2. How to Run a Eureka Server
    12.3. High Availability, Zones and Regions
    12.4. Standalone Mode
    12.5. Peer Awareness
    12.6. When to Prefer IP Address
    12.7. Securing The Eureka Server
    12.8. JDK 11 Support
    13. Circuit Breaker: Hystrix Clients
    13.1. How to Include Hystrix
    13.2. Propagating the Security Context or Using Spring Scopes
    13.3. Health Indicator
    13.4. Hystrix Metrics Stream
    14. Circuit Breaker: Hystrix Dashboard
    15. Hystrix Timeouts And Ribbon Clients
    15.1. How to Include the Hystrix Dashboard
    15.2. Turbine
    15.2.1. Clusters Endpoint
    15.3. Turbine Stream
    16. Client Side Load Balancer: Ribbon
    16.1. How to Include Ribbon
    16.2. Customizing the Ribbon Client
    16.3. Customizing the Default for All Ribbon Clients
    16.4. Customizing the Ribbon Client by Setting Properties
    16.5. Using Ribbon with Eureka
    16.6. Example: How to Use Ribbon Without Eureka
    16.7. Example: Disable Eureka Use in Ribbon
    16.8. Using the Ribbon API Directly
    16.9. Caching of Ribbon Configuration
    16.10. How to Configure Hystrix Thread Pools
    16.11. How to Provide a Key to Ribbon’s IRule
    17. External Configuration: Archaius
    18. Router and Filter: Zuul
    18.1. How to Include Zuul
    18.2. Embedded Zuul Reverse Proxy
    18.3. Zuul Http Client
    18.4. Cookies and Sensitive Headers
    18.5. Ignored Headers
    18.6. Management Endpoints
    18.6.1. Routes Endpoint
    18.6.2. Filters Endpoint
    18.7. Strangulation Patterns and Local Forwards
    18.8. Uploading Files through Zuul
    18.9. Query String Encoding
    18.10. Request URI Encoding
    18.11. Plain Embedded Zuul
    18.12. Disable Zuul Filters
    18.13. Providing Hystrix Fallbacks For Routes
    18.14. Zuul Timeouts
    18.15. Rewriting the Location header
    18.16. Enabling Cross Origin Requests
    18.17. Metrics
    18.18. Zuul Developer Guide
    18.18.1. The Zuul Servlet
    18.18.2. Zuul RequestContext
    18.18.3. @EnableZuulProxy vs. @EnableZuulServer
    18.18.4. @EnableZuulServer Filters
    18.18.5. @EnableZuulProxy Filters
    18.18.6. Custom Zuul Filter Examples
    How to Write a Pre Filter
    How to Write a Route Filter
    How to Write a Post Filter
    18.18.7. How Zuul Errors Work
    18.18.8. Zuul Eager Application Context Loading
    19. Polyglot support with Sidecar
    20. Retrying Failed Requests
    20.1. BackOff Policies
    20.2. Configuration
    20.2.1. Zuul
    21. HTTP Clients
    22. Modules In Maintenance Mode
    IV. Spring Cloud OpenFeign
    23. Declarative REST Client: Feign
    23.1. How to Include Feign
    23.2. Overriding Feign Defaults
    23.3. Creating Feign Clients Manually
    23.4. Feign Hystrix Support
    23.5. Feign Hystrix Fallbacks
    23.6. Feign and @Primary
    23.7. Feign Inheritance Support
    23.8. Feign request/response compression
    23.9. Feign logging
    23.10. Feign @QueryMap support
    23.11. Troubleshooting
    23.11.1. Early Initialization Errors
    V. Spring Cloud Stream
    24. A Brief History of Spring’s Data Integration Journey
    25. Quick Start
    25.1. Creating a Sample Application by Using Spring Initializr
    25.2. Importing the Project into Your IDE
    25.3. Adding a Message Handler, Building, and Running
    26. What’s New in 2.0?
    26.1. New Features and Components
    26.2. Notable Enhancements
    26.2.1. Both Actuator and Web Dependencies Are Now Optional
    26.2.2. Content-type Negotiation Improvements
    26.3. Notable Deprecations
    26.3.1. Java Serialization (Java Native and Kryo)
    26.3.2. Deprecated Classes and Methods
    27. Introducing Spring Cloud Stream
    28. Main Concepts
    28.1. Application Model
    28.1.1. Fat JAR
    28.2. The Binder Abstraction
    28.3. Persistent Publish-Subscribe Support
    28.4. Consumer Groups
    28.5. Consumer Types
    28.5.1. Durability
    28.6. Partitioning Support
    29. Programming Model
    29.1. Destination Binders
    29.2. Destination Bindings
    29.3. Producing and Consuming Messages
    29.3.1. Spring Integration Support
    29.3.2. Using @StreamListener Annotation
    29.3.3. Using @StreamListener for Content-based routing
    29.3.4. Spring Cloud Function support
    Functional Composition
    29.3.5. Using Polled Consumers
    Overview
    Handling Errors
    29.4. Error Handling
    29.4.1. Application Error Handling
    29.4.2. System Error Handling
    Drop Failed Messages
    DLQ - Dead Letter Queue
    Re-queue Failed Messages
    29.4.3. Retry Template
    29.5. Reactive Programming Support
    29.5.1. Reactor-based Handlers
    29.5.2. Reactive Sources
    30. Binders
    30.1. Producers and Consumers
    30.2. Binder SPI
    30.3. Binder Detection
    30.3.1. Classpath Detection
    30.4. Multiple Binders on the Classpath
    30.5. Connecting to Multiple Systems
    30.6. Binding visualization and control
    30.7. Binder Configuration Properties
    31. Configuration Options
    31.1. Binding Service Properties
    31.2. Binding Properties
    31.2.1. Common Binding Properties
    31.2.2. Consumer Properties
    31.2.3. Producer Properties
    31.3. Using Dynamically Bound Destinations
    32. Content Type Negotiation
    32.1. Mechanics
    32.1.1. Content Type versus Argument Type
    32.1.2. Message Converters
    32.2. Provided MessageConverters
    32.3. User-defined Message Converters
    33. Schema Evolution Support
    33.1. Schema Registry Client
    33.1.1. Schema Registry Client Properties
    33.2. Avro Schema Registry Client Message Converters
    33.2.1. Avro Schema Registry Message Converter Properties
    33.3. Apache Avro Message Converters
    33.4. Converters with Schema Support
    33.5. Schema Registry Server
    33.5.1. Schema Registry Server API
    Registering a New Schema
    Retrieving an Existing Schema by Subject, Format, and Version
    Retrieving an Existing Schema by Subject and Format
    Retrieving an Existing Schema by ID
    Deleting a Schema by Subject, Format, and Version
    Deleting a Schema by ID
    Deleting a Schema by Subject
    33.5.2. Using Confluent’s Schema Registry
    33.6. Schema Registration and Resolution
    33.6.1. Schema Registration Process (Serialization)
    33.6.2. Schema Resolution Process (Deserialization)
    34. Inter-Application Communication
    34.1. Connecting Multiple Application Instances
    34.2. Instance Index and Instance Count
    34.3. Partitioning
    34.3.1. Configuring Output Bindings for Partitioning
    34.3.2. Configuring Input Bindings for Partitioning
    35. Testing
    35.1. Disabling the Test Binder Autoconfiguration
    36. Health Indicator
    37. Metrics Emitter
    38. Samples
    38.1. Deploying Stream Applications on CloudFoundry
    VI. Binder Implementations
    39. Apache Kafka Binder
    39.1. Usage
    39.2. Apache Kafka Binder Overview
    39.3. Configuration Options
    39.3.1. Kafka Binder Properties
    39.3.2. Kafka Consumer Properties
    39.3.3. Kafka Producer Properties
    39.3.4. Usage examples
    Example: Setting autoCommitOffset to false and Relying on Manual Acking
    Example: Security Configuration
    Example: Pausing and Resuming the Consumer
    39.4. Error Channels
    39.5. Kafka Metrics
    39.6. Dead-Letter Topic Processing
    39.7. Partitioning with the Kafka Binder
    40. Apache Kafka Streams Binder
    40.1. Usage
    40.2. Kafka Streams Binder Overview
    40.2.1. Streams DSL
    40.3. Configuration Options
    40.3.1. Kafka Streams Properties
    40.3.2. TimeWindow properties:
    40.4. Multiple Input Bindings
    40.4.1. Multiple Input Bindings as a Sink
    40.4.2. Multiple Input Bindings as a Processor
    40.5. Multiple Output Bindings (aka Branching)
    40.6. Message Conversion
    40.6.1. Outbound serialization
    40.6.2. Inbound Deserialization
    40.7. Error Handling
    40.7.1. Handling Deserialization Exceptions
    40.7.2. Handling Non-Deserialization Exceptions
    40.8. State Store
    40.9. Interactive Queries
    40.10. Accessing the underlying KafkaStreams object
    40.11. State Cleanup
    41. RabbitMQ Binder
    41.1. Usage
    41.2. RabbitMQ Binder Overview
    41.3. Configuration Options
    41.3.1. RabbitMQ Binder Properties
    41.3.2. RabbitMQ Consumer Properties
    41.3.3. Advanced Listener Container Configuration
    41.3.4. Rabbit Producer Properties
    41.4. Retry With the RabbitMQ Binder
    41.4.1. Putting it All Together
    41.5. Error Channels
    41.6. Dead-Letter Queue Processing
    41.6.1. Non-Partitioned Destinations
    41.6.2. Partitioned Destinations
    republishToDlq=false
    republishToDlq=true
    41.7. Partitioning with the RabbitMQ Binder
    VII. Spring Cloud Bus
    42. Quick Start
    43. Bus Endpoints
    43.1. Bus Refresh Endpoint
    43.2. Bus Env Endpoint
    44. Addressing an Instance
    45. Addressing All Instances of a Service
    46. Service ID Must Be Unique
    47. Customizing the Message Broker
    48. Tracing Bus Events
    49. Broadcasting Your Own Events
    49.1. Registering events in custom packages
    VIII. Spring Cloud Sleuth
    50. Introduction
    50.1. Terminology
    50.2. Purpose
    50.2.1. Distributed Tracing with Zipkin
    50.2.2. Visualizing errors
    50.2.3. Distributed Tracing with Brave
    50.2.4. Live examples
    50.2.5. Log correlation
    JSON Logback with Logstash
    50.2.6. Propagating Span Context
    Baggage versus Span Tags
    50.3. Adding Sleuth to the Project
    50.3.1. Only Sleuth (log correlation)
    50.3.2. Sleuth with Zipkin via HTTP
    50.3.3. Sleuth with Zipkin over RabbitMQ or Kafka
    50.4. Overriding the auto-configuration of Zipkin
    51. Additional Resources
    52. Features
    52.1. Introduction to Brave
    52.1.1. Tracing
    52.1.2. Local Tracing
    52.1.3. Customizing Spans
    52.1.4. Implicitly Looking up the Current Span
    52.1.5. RPC tracing
    One-Way tracing
    53. Sampling
    53.1. Declarative sampling
    53.2. Custom sampling
    53.3. Sampling in Spring Cloud Sleuth
    54. Propagation
    54.1. Propagating extra fields
    54.1.1. Prefixed fields
    54.1.2. Extracting a Propagated Context
    54.1.3. Sharing span IDs between Client and Server
    54.1.4. Implementing Propagation
    55. Current Tracing Component
    56. Current Span
    56.1. Setting a span in scope manually
    57. Instrumentation
    58. Span lifecycle
    58.1. Creating and finishing spans
    58.2. Continuing Spans
    58.3. Creating a Span with an explicit Parent
    59. Naming spans
    59.1. @SpanName Annotation
    59.2. toString() method
    60. Managing Spans with Annotations
    60.1. Rationale
    60.2. Creating New Spans
    60.3. Continuing Spans
    60.4. Advanced Tag Setting
    60.4.1. Custom extractor
    60.4.2. Resolving Expressions for a Value
    60.4.3. Using the toString() method
    61. Customizations
    61.1. Customizers
    61.2. HTTP
    61.3. TracingFilter
    61.4. RPC
    61.5. Custom service name
    61.6. Customization of Reported Spans
    61.7. Host Locator
    62. Sending Spans to Zipkin
    63. Zipkin Stream Span Consumer
    64. Integrations
    64.1. OpenTracing
    64.2. Runnable and Callable
    64.3. Hystrix
    64.3.1. Custom Concurrency Strategy
    64.3.2. Manual Command setting
    64.4. RxJava
    64.5. HTTP integration
    64.5.1. HTTP Filter
    64.5.2. HandlerInterceptor
    64.5.3. Async Servlet support
    64.5.4. WebFlux support
    64.5.5. Dubbo RPC support
    64.6. HTTP Client Integration
    64.6.1. Synchronous Rest Template
    64.6.2. Asynchronous Rest Template
    Multiple Asynchronous Rest Templates
    64.6.3. WebClient
    64.6.4. Traverson
    64.6.5. Apache HttpClientBuilder and HttpAsyncClientBuilder
    64.6.6. Netty HttpClient
    64.6.7. UserInfoRestTemplateCustomizer
    64.7. Feign
    64.8. gRPC
    64.8.1. Variant 1
    Dependencies
    Server Instrumentation
    Client Instrumentation
    64.8.2. Variant 2
    64.9. Asynchronous Communication
    64.9.1. @Async Annotated methods
    64.9.2. @Scheduled Annotated Methods
    64.9.3. Executor, ExecutorService, and ScheduledExecutorService
    Customization of Executors
    64.10. Messaging
    64.10.1. Spring Integration and Spring Cloud Stream
    64.10.2. Spring RabbitMq
    64.10.3. Spring Kafka
    64.10.4. Spring JMS
    64.11. Zuul
    64.12. Project Reactor
    65. Running examples
    IX. Spring Cloud Consul
    66. Install Consul
    67. Consul Agent
    68. Service Discovery with Consul
    68.1. How to activate
    68.2. Registering with Consul
    68.2.1. Registering Management as a Separate Service
    68.3. HTTP Health Check
    68.3.1. Metadata and Consul tags
    68.3.2. Making the Consul Instance ID Unique
    68.3.3. Applying Headers to Health Check Requests
    68.4. Looking up services
    68.4.1. Using Ribbon
    68.4.2. Using the DiscoveryClient
    68.5. Consul Catalog Watch
    69. Distributed Configuration with Consul
    69.1. How to activate
    69.2. Customizing
    69.3. Config Watch
    69.4. YAML or Properties with Config
    69.5. git2consul with Config
    69.6. Fail Fast
    70. Consul Retry
    71. Spring Cloud Bus with Consul
    71.1. How to activate
    72. Circuit Breaker with Hystrix
    73. Hystrix metrics aggregation with Turbine and Consul
    X. Spring Cloud Zookeeper
    74. Install Zookeeper
    75. Service Discovery with Zookeeper
    75.1. Activating
    75.2. Registering with Zookeeper
    75.3. Using the DiscoveryClient
    76. Using Spring Cloud Zookeeper with Spring Cloud Netflix Components
    76.1. Ribbon with Zookeeper
    77. Spring Cloud Zookeeper and Service Registry
    77.1. Instance Status
    78. Zookeeper Dependencies
    78.1. Using the Zookeeper Dependencies
    78.2. Activating Zookeeper Dependencies
    78.3. Setting up Zookeeper Dependencies
    78.3.1. Aliases
    78.3.2. Path
    78.3.3. Load Balancer Type
    78.3.4. Content-Type Template and Version
    78.3.5. Default Headers
    78.3.6. Required Dependencies
    78.3.7. Stubs
    78.4. Configuring Spring Cloud Zookeeper Dependencies
    79. Spring Cloud Zookeeper Dependency Watcher
    79.1. Activating
    79.2. Registering a Listener
    79.3. Using the Presence Checker
    80. Distributed Configuration with Zookeeper
    80.1. Activating
    80.2. Customizing
    80.3. Access Control Lists (ACLs)
    XI. Spring Cloud Security
    81. Quickstart
    81.1. OAuth2 Single Sign On
    81.2. OAuth2 Protected Resource
    82. More Detail
    82.1. Single Sign On
    82.2. Token Relay
    82.2.1. Client Token Relay in Spring Cloud Gateway
    82.2.2. Client Token Relay
    82.2.3. Client Token Relay in Zuul Proxy
    82.2.4. Resource Server Token Relay
    83. Configuring Authentication Downstream of a Zuul Proxy
    XII. Spring Cloud for Cloud Foundry
    84. Discovery
    85. Single Sign On
    XIII. Spring Cloud Contract
    86. Spring Cloud Contract
    87. Spring Cloud Contract Verifier Introduction
    87.1. History
    87.2. Why a Contract Verifier?
    87.2.1. Testing issues
    87.3. Purposes
    87.4. How It Works
    87.4.1. A Three-second Tour
    On the Producer Side
    On the Consumer Side
    87.4.2. A Three-minute Tour
    On the Producer Side
    On the Consumer Side
    87.4.3. Defining the Contract
    87.4.4. Client Side
    87.4.5. Server Side
    87.5. Step-by-step Guide to Consumer Driven Contracts (CDC)
    87.5.1. Technical note
    87.5.2. Consumer side (Loan Issuance)
    87.5.3. Producer side (Fraud Detection server)
    87.5.4. Consumer Side (Loan Issuance) Final Step
    87.6. Dependencies
    87.7. Additional Links
    87.7.1. Spring Cloud Contract video
    87.7.2. Readings
    87.8. Samples
    88. Spring Cloud Contract FAQ
    88.1. Why use Spring Cloud Contract Verifier and not X ?
    88.2. I don’t want to write a contract in Groovy!
    88.3. What is this value(consumer(), producer()) ?
    88.4. How to do Stubs versioning?
    88.4.1. API Versioning
    88.4.2. JAR versioning
    88.4.3. Dev or prod stubs
    88.5. Common repo with contracts
    88.5.1. Repo structure
    88.5.2. Workflow
    88.5.3. Consumer
    88.5.4. Producer
    88.5.5. How can I define messaging contracts per topic not per producer?
    For Maven Project
    For Gradle Project
    88.6. Do I need a Binary Storage? Can’t I use Git?
    88.6.1. Protocol convention
    88.6.2. Producer
    88.6.3. Producer with contracts stored locally
    Keeping contracts with the producer and stubs in an external repository
    88.6.4. Consumer
    88.7. Can I use the Pact Broker?
    88.7.1. Pact Consumer
    88.7.2. Producer
    88.7.3. Pact Consumer (Producer Contract approach)
    88.8. How can I debug the request/response being sent by the generated tests client?
    88.8.1. How can I debug the mapping/request/response being sent by WireMock?
    88.8.2. How can I see what got registered in the HTTP server stub?
    88.8.3. Can I reference text from file?
    89. Spring Cloud Contract Verifier Setup
    89.1. Gradle Project
    89.1.1. Prerequisites
    90. Add Gradle Plugin with Dependencies
    90.1. Gradle and Rest Assured 2.0
    90.2. Snapshot Versions for Gradle
    90.3. Add stubs
    90.4. Run the Plugin
    90.5. Default Setup
    90.6. Configure Plugin
    90.7. Configuration Options
    90.8. Single Base Class for All Tests
    90.9. Different Base Classes for Contracts
    90.10. Invoking Generated Tests
    90.11. Pushing stubs to SCM
    90.12. Spring Cloud Contract Verifier on the Consumer Side
    90.13. Maven Project
    90.13.1. Add maven plugin
    90.13.2. Maven and Rest Assured 2.0
    90.13.3. Snapshot versions for Maven
    90.13.4. Add stubs
    90.13.5. Run plugin
    90.13.6. Configure plugin
    90.13.7. Configuration Options
    90.13.8. Single Base Class for All Tests
    90.13.9. Different base classes for contracts
    90.13.10. Invoking generated tests
    90.13.11. Pushing stubs to SCM
    90.13.12. Maven Plugin and STS
    90.13.13. Maven Plugin with Spock Tests
    90.14. Stubs and Transitive Dependencies
    90.15. Scenarios
    90.16. Docker Project
    90.16.1. Short intro to Maven, JARs and Binary storage
    90.16.2. How it works
    Environment Variables
    90.16.3. Example of usage
    90.16.4. Server side (nodejs)
    91. Spring Cloud Contract Verifier Messaging
    91.1. Integrations
    91.2. Manual Integration Testing
    91.3. Publisher-Side Test Generation
    91.3.1. Scenario 1: No Input Message
    91.3.2. Scenario 2: Output Triggered by Input
    91.3.3. Scenario 3: No Output Message
    91.4. Consumer Stub Generation
    92. Spring Cloud Contract Stub Runner
    92.1. Snapshot versions
    92.2. Publishing Stubs as JARs
    92.3. Stub Runner Core
    92.3.1. Retrieving stubs
    Stub downloading
    Classpath scanning
    Configuring HTTP Server Stubs
    92.3.2. Running stubs
    Running using main app
    HTTP Stubs
    Viewing registered mappings
    Messaging Stubs
    92.4. Stub Runner JUnit Rule and Stub Runner JUnit5 Extension
    92.4.1. Maven settings
    92.4.2. Providing fixed ports
    92.4.3. Fluent API
    92.4.4. Stub Runner with Spring
    92.5. Stub Runner Spring Cloud
    92.5.1. Stubbing Service Discovery
    Test profiles and service discovery
    92.5.2. Additional Configuration
    92.6. Stub Runner Boot Application
    92.6.1. How to use it?
    Stub Runner Server
    Stub Runner Server Fat Jar
    Spring Cloud CLI
    92.6.2. Endpoints
    HTTP
    Messaging
    92.6.3. Example
    92.6.4. Stub Runner Boot with Service Discovery
    92.7. Stubs Per Consumer
    92.8. Common
    92.8.1. Common Properties for JUnit and Spring
    92.8.2. Stub Runner Stubs IDs
    92.9. Stub Runner Docker
    92.9.1. How to use it
    92.9.2. Example of client side usage in a non JVM project
    93. Stub Runner for Messaging
    93.1. Stub triggering
    93.1.1. Trigger by Label
    93.1.2. Trigger by Group and Artifact Ids
    93.1.3. Trigger by Artifact Ids
    93.1.4. Trigger All Messages
    93.2. Stub Runner Camel
    93.2.1. Adding it to the project
    93.2.2. Disabling the functionality
    93.2.3. Examples
    Stubs structure
    Scenario 1 (no input message)
    Scenario 2 (output triggered by input)
    Scenario 3 (input with no output)
    93.3. Stub Runner Integration
    93.3.1. Adding the Runner to the Project
    93.3.2. Disabling the functionality
    Scenario 1 (no input message)
    Scenario 2 (output triggered by input)
    Scenario 3 (input with no output)
    93.4. Stub Runner Stream
    93.4.1. Adding the Runner to the Project
    93.4.2. Disabling the functionality
    Scenario 1 (no input message)
    Scenario 2 (output triggered by input)
    Scenario 3 (input with no output)
    93.5. Stub Runner Spring AMQP
    93.5.1. Adding the Runner to the Project
    Triggering the message
    Spring AMQP Test Configuration
    94. Contract DSL
    94.1. Limitations
    94.2. Common Top-Level elements
    94.2.1. Description
    94.2.2. Name
    94.2.3. Ignoring Contracts
    94.2.4. Passing Values from Files
    94.2.5. HTTP Top-Level Elements
    94.3. Request
    94.4. Response
    94.5. Dynamic properties
    94.5.1. Dynamic properties inside the body
    94.5.2. Regular expressions
    94.5.3. Passing Optional Parameters
    94.5.4. Executing Custom Methods on the Server Side
    94.5.5. Referencing the Request from the Response
    94.5.6. Registering Your Own WireMock Extension
    94.5.7. Dynamic Properties in the Matchers Sections
    94.6. JAX-RS Support
    94.7. Async Support
    94.8. Working with Context Paths
    94.9. Working with WebFlux
    94.9.1. WebFlux with WebTestClient
    94.9.2. WebFlux with Explicit mode
    94.10. XML Support for REST
    94.11. Messaging Top-Level Elements
    94.11.1. Output Triggered by a Method
    94.11.2. Output Triggered by a Message
    94.11.3. Consumer/Producer
    94.11.4. Common
    94.12. Multiple Contracts in One File
    94.13. Generating Spring REST Docs snippets from the contracts
    95. Customization
    95.1. Extending the DSL
    95.1.1. Common JAR
    95.1.2. Adding the Dependency to the Project
    95.1.3. Test the Dependency in the Project’s Dependencies
    95.1.4. Test a Dependency in the Plugin’s Dependencies
    95.1.5. Referencing classes in DSLs
    96. Using the Pluggable Architecture
    96.1. Custom Contract Converter
    96.1.1. Pact Converter
    96.1.2. Pact Contract
    96.1.3. Pact for Producers
    96.1.4. Pact for Consumers
    96.2. Using the Custom Test Generator
    96.3. Using the Custom Stub Generator
    96.4. Using the Custom Stub Runner
    96.5. Using the Custom Stub Downloader
    96.6. Using the SCM Stub Downloader
    96.7. Using the Pact Stub Downloader
    97. Spring Cloud Contract WireMock
    97.1. Registering Stubs Automatically
    97.2. Using Files to Specify the Stub Bodies
    97.3. Alternative: Using JUnit Rules
    97.4. Relaxed SSL Validation for Rest Template
    97.5. WireMock and Spring MVC Mocks
    97.6. Customization of WireMock configuration
    97.7. Generating Stubs using REST Docs
    97.8. Generating Contracts by Using REST Docs
    98. Migrations
    98.1. 1.0.x → 1.1.x
    98.1.1. New structure of generated stubs
    98.2. 1.1.x → 1.2.x
    98.2.1. Custom HttpServerStub
    98.2.2. New packages for generated tests
    98.2.3. New Methods in TemplateProcessor
    98.2.4. RestAssured 3.0
    98.3. 1.2.x → 2.0.x
    99. Links
    XIV. Spring Cloud Vault
    100. Quick Start
    101. Client Side Usage
    101.1. Authentication
    102. Authentication methods
    102.1. Token authentication
    102.2. AppId authentication
    102.2.1. Custom UserId
    102.3. AppRole authentication
    102.4. AWS-EC2 authentication
    102.5. AWS-IAM authentication
    102.6. Azure MSI authentication
    102.7. TLS certificate authentication
    102.8. Cubbyhole authentication
    102.9. GCP-GCE authentication
    102.10. GCP-IAM authentication
    102.11. Kubernetes authentication
    103. Secret Backends
    103.1. Generic Backend
    103.2. Versioned Key-Value Backend
    103.3. Consul
    103.4. RabbitMQ
    103.5. AWS
    104. Database backends
    104.1. Database
    104.2. Apache Cassandra
    104.3. MongoDB
    104.4. MySQL
    104.5. PostgreSQL
    105. Configure PropertySourceLocator behavior
    106. Service Registry Configuration
    107. Vault Client Fail Fast
    108. Vault Client SSL configuration
    109. Lease lifecycle management (renewal and revocation)
    XV. Spring Cloud Gateway
    110. How to Include Spring Cloud Gateway
    111. Glossary
    112. How It Works
    113. Configuring Route Predicate Factories and Gateway Filter Factories
    113.1. Shortcut Configuration
    113.2. Fully Expanded Arguments
    114. Route Predicate Factories
    114.1. After Route Predicate Factory
    114.2. Before Route Predicate Factory
    114.3. Between Route Predicate Factory
    114.4. Cookie Route Predicate Factory
    114.5. Header Route Predicate Factory
    114.6. Host Route Predicate Factory
    114.7. Method Route Predicate Factory
    114.8. Path Route Predicate Factory
    114.9. Query Route Predicate Factory
    114.10. RemoteAddr Route Predicate Factory
    114.11. Weight Route Predicate Factory
    114.11.1. Modifying the way remote addresses are resolved
    115. GatewayFilter Factories
    115.1. AddRequestHeader GatewayFilter Factory
    115.2. AddRequestParameter GatewayFilter Factory
    115.3. AddResponseHeader GatewayFilter Factory
    115.4. DedupeResponseHeader GatewayFilter Factory
    115.5. Hystrix GatewayFilter Factory
    115.6. FallbackHeaders GatewayFilter Factory
    115.7. MapRequestHeader GatewayFilter Factory
    115.8. PrefixPath GatewayFilter Factory
    115.9. PreserveHostHeader GatewayFilter Factory
    115.10. RequestRateLimiter GatewayFilter Factory
    115.10.1. Redis RateLimiter
    115.11. RedirectTo GatewayFilter Factory
    115.12. RemoveRequestHeader GatewayFilter Factory
    115.13. RemoveResponseHeader GatewayFilter Factory
    115.14. RewritePath GatewayFilter Factory
    115.15. RewriteLocationResponseHeader GatewayFilter Factory
    115.16. RewriteResponseHeader GatewayFilter Factory
    115.17. SaveSession GatewayFilter Factory
    115.18. SecureHeaders GatewayFilter Factory
    115.19. SetPath GatewayFilter Factory
    115.20. SetRequestHeader GatewayFilter Factory
    115.21. SetResponseHeader GatewayFilter Factory
    115.22. SetStatus GatewayFilter Factory
    115.23. StripPrefix GatewayFilter Factory
    115.24. Retry GatewayFilter Factory
    115.25. RequestSize GatewayFilter Factory
    115.26. Modify Request Body GatewayFilter Factory
    115.27. Modify Response Body GatewayFilter Factory
    115.28. Default Filters
    116. Global Filters
    116.1. Combined Global Filter and GatewayFilter Ordering
    116.2. Forward Routing Filter
    116.3. LoadBalancerClient Filter
    116.4. ReactiveLoadBalancerClientFilter
    116.5. Netty Routing Filter
    116.6. Netty Write Response Filter
    116.7. RouteToRequestUrl Filter
    116.8. Websocket Routing Filter
    116.9. Gateway Metrics Filter
    116.10. Marking An Exchange As Routed
    117. HttpHeadersFilters
    117.1. Forwarded Headers Filter
    117.2. RemoveHopByHop Headers Filter
    117.3. XForwarded Headers Filter
    118. TLS / SSL
    118.1. TLS Handshake
    119. Configuration
    119.1. Fluent Java Routes API
    119.2. DiscoveryClient Route Definition Locator
    119.2.1. Configuring Predicates and Filters For DiscoveryClient Routes
    120. Reactor Netty Access Logs
    121. CORS Configuration
    122. Actuator API
    122.1. Verbose Actuator Format
    122.2. Retrieving route filters
    122.2.1. Global Filters
    122.2.2. Route Filters
    122.3. Refreshing the route cache
    122.4. Retrieving the routes defined in the gateway
    122.5. Retrieving information about a particular route
    122.6. Creating and deleting a particular route
    122.7. Recap: list of all endpoints
    123. Troubleshooting
    123.1. Log Levels
    123.2. Wiretap
    124. Developer Guide
    124.1. Writing Custom Route Predicate Factories
    124.2. Writing Custom GatewayFilter Factories
    124.3. Writing Custom Global Filters
    125. Building a Simple Gateway Using Spring MVC or Webflux
    XVI. Spring Cloud Function
    126. Introduction
    127. Getting Started
    128. Building and Running a Function
    129. Function Catalog and Flexible Function Signatures
    129.1. Java 8 function support
    129.2. Kotlin Lambda support
    130. Standalone Web Applications
    131. Standalone Streaming Applications
    132. Deploying a Packaged Function
    133. Functional Bean Definitions
    133.1. Comparing Functional with Traditional Bean Definitions
    133.2. Testing Functional Applications
    133.3. Limitations of Functional Bean Declaration
    134. Dynamic Compilation
    135. Serverless Platform Adapters
    135.1. AWS Lambda
    135.1.1. Introduction
    135.1.2. Notes on JAR Layout
    135.1.3. Upload
    135.1.4. Platfom Specific Features
    HTTP and API Gateway
    135.2. Azure Functions
    135.2.1. Notes on JAR Layout
    135.2.2. Build
    135.2.3. Running the sample
    135.3. Apache Openwhisk
    135.3.1. Quick Start
    XVII. Spring Cloud Kubernetes
    136. Why do you need Spring Cloud Kubernetes?
    137. Starters
    138. DiscoveryClient for Kubernetes
    139. Kubernetes native service discovery
    140. Kubernetes PropertySource implementations
    140.1. Using a ConfigMap PropertySource
    140.2. Secrets PropertySource
    140.3. PropertySource Reload
    141. Ribbon Discovery in Kubernetes
    142. Kubernetes Ecosystem Awareness
    142.1. Kubernetes Profile Autoconfiguration
    142.2. Istio Awareness
    143. Pod Health Indicator
    144. Leader Election
    145. Security Configurations Inside Kubernetes
    145.1. Namespace
    145.2. Service Account
    146. Service Registry Implementation
    147. Examples
    148. Other Resources
    149. Building
    149.1. Basic Compile and Test
    149.2. Documentation
    149.3. Working with the code
    149.3.1. Importing into eclipse with m2eclipse
    149.3.2. Importing into eclipse without m2eclipse
    150. Contributing
    150.1. Sign the Contributor License Agreement
    150.2. Code of Conduct
    150.3. Code Conventions and Housekeeping
    150.4. Checkstyle
    150.4.1. Checkstyle configuration
    150.5. IDE setup
    150.5.1. Intellij IDEA
    XVIII. Spring Cloud GCP
    151. Introduction
    152. Dependency Management
    153. Getting started
    153.1. Spring Initializr
    153.1.1. GCP Support
    153.1.2. GCP Messaging
    153.1.3. GCP Storage
    153.2. Code Samples
    153.3. Code Challenges
    153.4. Getting Started Guides
    154. Spring Cloud GCP Core
    154.1. Project ID
    154.2. Credentials
    154.2.1. Scopes
    154.3. Environment
    154.4. Spring Initializr
    155. Google Cloud Pub/Sub
    155.1. Pub/Sub Operations & Template
    155.1.1. Publishing to a topic
    JSON support
    155.1.2. Subscribing to a subscription
    155.1.3. Pulling messages from a subscription
    155.2. Pub/Sub management
    155.2.1. Creating a topic
    155.2.2. Deleting a topic
    155.2.3. Listing topics
    155.2.4. Creating a subscription
    155.2.5. Deleting a subscription
    155.2.6. Listing subscriptions
    155.3. Configuration
    155.4. Sample
    156. Spring Resources
    156.1. Google Cloud Storage
    156.1.1. Setting the Content Type
    156.2. Configuration
    156.3. Sample
    157. Spring JDBC
    157.1. Prerequisites
    157.2. Spring Boot Starter for Google Cloud SQL
    157.2.1. DataSource creation flow
    157.2.2. Troubleshooting tips
    Connection issues
    Errors like c.g.cloud.sql.core.SslSocketFactory : Re-throwing cached exception due to attempt to refresh instance information too soon after error
    PostgreSQL: java.net.SocketException: already connected issue
    157.3. Samples
    158. Spring Integration
    158.1. Channel Adapters for Cloud Pub/Sub
    158.1.1. Inbound channel adapter
    158.1.2. Outbound channel adapter
    158.1.3. Header mapping
    158.2. Sample
    158.3. Channel Adapters for Google Cloud Storage
    158.3.1. Inbound channel adapter
    158.3.2. Inbound streaming channel adapter
    158.3.3. Outbound channel adapter
    158.4. Sample
    159. Spring Cloud Stream
    159.1. Overview
    159.2. Configuration
    159.2.1. Producer Destination Configuration
    159.2.2. Consumer Destination Configuration
    159.3. Sample
    160. Spring Cloud Sleuth
    160.1. Tracing
    160.2. Spring Boot Starter for Stackdriver Trace
    160.3. Overriding the auto-configuration
    160.4. Integration with Logging
    160.5. Sample
    161. Stackdriver Logging
    161.1. Web MVC Interceptor
    161.2. Logback Support
    161.2.1. Log via API
    161.2.2. Log via Console
    161.3. Sample
    162. Spring Cloud Config
    162.1. Configuration
    162.2. Quick start
    162.3. Refreshing the configuration at runtime
    162.4. Sample
    163. Spring Data Cloud Spanner
    163.1. Configuration
    163.1.1. Cloud Spanner settings
    163.1.2. Repository settings
    163.1.3. Autoconfiguration
    163.2. Object Mapping
    163.2.1. Constructors
    163.2.2. Table
    SpEL expressions for table names
    163.2.3. Primary Keys
    163.2.4. Columns
    163.2.5. Embedded Objects
    163.2.6. Relationships
    163.2.7. Supported Types
    163.2.8. Lists
    163.2.9. Lists of Structs
    163.2.10. Custom types
    163.2.11. Custom Converter for Struct Array Columns
    163.3. Spanner Operations & Template
    163.3.1. SQL Query
    163.3.2. Read
    163.3.3. Advanced reads
    Stale read
    Read from a secondary index
    Read with offsets and limits
    Sorting
    Partial read
    Summary of options for Query vs Read
    163.3.4. Write / Update
    Insert
    Update
    Upsert
    Partial Update
    163.3.5. DML
    163.3.6. Transactions
    Read/Write Transaction
    Read-only Transaction
    Declarative Transactions with @Transactional Annotation
    163.3.7. DML Statements
    163.4. Repositories
    163.4.1. CRUD Repository
    163.4.2. Paging and Sorting Repository
    163.4.3. Spanner Repository
    163.5. Query Methods
    163.5.1. Query methods by convention
    163.5.2. Custom SQL/DML query methods
    Query methods with named queries properties
    Query methods with annotation
    163.5.3. Projections
    163.5.4. REST Repositories
    163.6. Database and Schema Admin
    163.7. Sample
    164. Spring Data Cloud Datastore
    164.1. Configuration
    164.1.1. Cloud Datastore settings
    164.1.2. Repository settings
    164.1.3. Autoconfiguration
    164.2. Object Mapping
    164.2.1. Constructors
    164.2.2. Kind
    164.2.3. Keys
    164.2.4. Fields
    164.2.5. Supported Types
    164.2.6. Custom types
    164.2.7. Collections and arrays
    164.2.8. Custom Converter for collections
    164.3. Relationships
    164.3.1. Embedded Entities
    Maps
    164.3.2. Ancestor-Descendant Relationships
    164.3.3. Key Reference Relationships
    164.4. Datastore Operations & Template
    164.4.1. GQL Query
    164.4.2. Find by ID(s)
    Indexes
    Read with offsets, limits, and sorting
    Partial read
    164.4.3. Write / Update
    Partial Update
    164.4.4. Transactions
    Declarative Transactions with @Transactional Annotation
    164.4.5. Read-Write Support for Maps
    164.5. Repositories
    164.5.1. Query methods by convention
    164.5.2. Custom GQL query methods
    Query methods with annotation
    Query methods with named queries properties
    164.5.3. Transactions
    164.5.4. Projections
    164.5.5. REST Repositories
    164.6. Sample
    165. Cloud Memorystore for Redis
    165.1. Spring Caching
    166. Cloud Identity-Aware Proxy (IAP) Authentication
    166.1. Configuration
    166.2. Sample
    167. Google Cloud Vision
    167.1. Cloud Vision Template
    167.2. Detect Image Labels Example
    167.3. Sample
    168. Cloud Foundry
    169. Kotlin Support
    169.1. Prerequisites
    170. Sample
    XIX. Appendix: Compendium of Configuration Properties
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_stub-runner-for-messaging.html b/Greenwich.SR5/multi/multi_stub-runner-for-messaging.html new file mode 100644 index 00000000..5a86371a --- /dev/null +++ b/Greenwich.SR5/multi/multi_stub-runner-for-messaging.html @@ -0,0 +1,360 @@ + + + 93. Stub Runner for Messaging

    93. Stub Runner for Messaging

    Stub Runner can run the published stubs in memory. It can integrate with the following +frameworks:

    • Spring Integration
    • Spring Cloud Stream
    • Apache Camel
    • Spring AMQP

    It also provides entry points to integrate with any other solution on the market.

    [Important]Important

    If you have multiple frameworks on the classpath Stub Runner will need to +define which one should be used. Let’s assume that you have both AMQP, Spring Cloud Stream and Spring Integration +on the classpath. Then you need to set stubrunner.stream.enabled=false and stubrunner.integration.enabled=false. +That way the only remaining framework is Spring AMQP.

    93.1 Stub triggering

    To trigger a message, use the StubTrigger interface:

    package org.springframework.cloud.contract.stubrunner;
    +
    +import java.util.Collection;
    +import java.util.Map;
    +
    +/**
    + * Contract for triggering stub messages.
    + *
    + * @author Marcin Grzejszczak
    + */
    +public interface StubTrigger {
    +
    +	/**
    +	 * Triggers an event by a given label for a given {@code groupid:artifactid} notation.
    +	 * You can use only {@code artifactId} too.
    +	 *
    +	 * Feature related to messaging.
    +	 * @param ivyNotation ivy notation of a stub
    +	 * @param labelName name of the label to trigger
    +	 * @return true - if managed to run a trigger
    +	 */
    +	boolean trigger(String ivyNotation, String labelName);
    +
    +	/**
    +	 * Triggers an event by a given label.
    +	 *
    +	 * Feature related to messaging.
    +	 * @param labelName name of the label to trigger
    +	 * @return true - if managed to run a trigger
    +	 */
    +	boolean trigger(String labelName);
    +
    +	/**
    +	 * Triggers all possible events.
    +	 *
    +	 * Feature related to messaging.
    +	 * @return true - if managed to run a trigger
    +	 */
    +	boolean trigger();
    +
    +	/**
    +	 * Feature related to messaging.
    +	 * @return a mapping of ivy notation of a dependency to all the labels it has.
    +	 */
    +	Map<String, Collection<String>> labels();
    +
    +}

    For convenience, the StubFinder interface extends StubTrigger, so you only need one +or the other in your tests.

    StubTrigger gives you the following options to trigger a message:

    93.1.1 Trigger by Label

    stubFinder.trigger('return_book_1')

    93.1.2 Trigger by Group and Artifact Ids

    stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1')

    93.1.3 Trigger by Artifact Ids

    stubFinder.trigger('streamService', 'return_book_1')

    93.1.4 Trigger All Messages

    stubFinder.trigger()

    93.2 Stub Runner Camel

    Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to integrate with Apache Camel. +For the provided artifacts it will automatically download the stubs and register the required +routes.

    93.2.1 Adding it to the project

    It’s enough to have both Apache Camel and Spring Cloud Contract Stub Runner on classpath. +Remember to annotate your test class with @AutoConfigureStubRunner.

    93.2.2 Disabling the functionality

    If you need to disable this functionality just pass stubrunner.camel.enabled=false property.

    93.2.3 Examples

    Stubs structure

    Let us assume that we have the following Maven repository with a deployed stubs for the +camelService application.

    └── .m2
    +    └── repository
    +        └── io
    +            └── codearte
    +                └── accurest
    +                    └── stubs
    +                        └── camelService
    +                            ├── 0.0.1-SNAPSHOT
    +                            │   ├── camelService-0.0.1-SNAPSHOT.pom
    +                            │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar
    +                            │   └── maven-metadata-local.xml
    +                            └── maven-metadata-local.xml

    And the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── repository
    +    ├── accurest
    +    │   ├── bookDeleted.groovy
    +    │   ├── bookReturned1.groovy
    +    │   └── bookReturned2.groovy
    +    └── mappings

    Let’s consider the following contracts (let' number it with 1):

    Contract.make {
    +	label 'return_book_1'
    +	input {
    +		triggeredBy('bookReturnedTriggered()')
    +	}
    +	outputMessage {
    +		sentTo('jms:output')
    +		body('''{ "bookName" : "foo" }''')
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    and number 2

    Contract.make {
    +	label 'return_book_2'
    +	input {
    +		messageFrom('jms:input')
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		messageHeaders {
    +			header('sample', 'header')
    +		}
    +	}
    +	outputMessage {
    +		sentTo('jms:output')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    Scenario 1 (no input message)

    So as to trigger a message via the return_book_1 label we’ll use the StubTigger interface as follows

    stubFinder.trigger('return_book_1')

    Next we’ll want to listen to the output of the message sent to jms:output

    Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)

    And the received message would pass the following assertions

    receivedMessage != null
    +assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
    +receivedMessage.in.headers.get('BOOK-NAME') == 'foo'

    Scenario 2 (output triggered by input)

    Since the route is set for you it’s enough to just send a message to the jms:output destination.

    producerTemplate.
    +		sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])

    Next we’ll want to listen to the output of the message sent to jms:output

    Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)

    And the received message would pass the following assertions

    receivedMessage != null
    +assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
    +receivedMessage.in.headers.get('BOOK-NAME') == 'foo'

    Scenario 3 (input with no output)

    Since the route is set for you it’s enough to just send a message to the jms:output destination.

    producerTemplate.
    +		sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])

    93.3 Stub Runner Integration

    Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to +integrate with Spring Integration. For the provided artifacts, it automatically downloads +the stubs and registers the required routes.

    93.3.1 Adding the Runner to the Project

    You can have both Spring Integration and Spring Cloud Contract Stub Runner on the +classpath. Remember to annotate your test class with @AutoConfigureStubRunner.

    93.3.2 Disabling the functionality

    If you need to disable this functionality, set the +stubrunner.integration.enabled=false property.

    Assume that you have the following Maven repository with deployed stubs for the +integrationService application:

    └── .m2
    +    └── repository
    +        └── io
    +            └── codearte
    +                └── accurest
    +                    └── stubs
    +                        └── integrationService
    +                            ├── 0.0.1-SNAPSHOT
    +                            │   ├── integrationService-0.0.1-SNAPSHOT.pom
    +                            │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
    +                            │   └── maven-metadata-local.xml
    +                            └── maven-metadata-local.xml

    Further assume the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── repository
    +    ├── accurest
    +    │   ├── bookDeleted.groovy
    +    │   ├── bookReturned1.groovy
    +    │   └── bookReturned2.groovy
    +    └── mappings

    Consider the following contracts (numbered 1):

    Contract.make {
    +	label 'return_book_1'
    +	input {
    +		triggeredBy('bookReturnedTriggered()')
    +	}
    +	outputMessage {
    +		sentTo('output')
    +		body('''{ "bookName" : "foo" }''')
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    Now consider 2:

    Contract.make {
    +	label 'return_book_2'
    +	input {
    +		messageFrom('input')
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		messageHeaders {
    +			header('sample', 'header')
    +		}
    +	}
    +	outputMessage {
    +		sentTo('output')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    and the following Spring Integration Route:

    <?xml version="1.0" encoding="UTF-8"?>
    +<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +			 xmlns:beans="http://www.springframework.org/schema/beans"
    +			 xmlns="http://www.springframework.org/schema/integration"
    +			 xsi:schemaLocation="http://www.springframework.org/schema/beans
    +			https://www.springframework.org/schema/beans/spring-beans.xsd
    +			http://www.springframework.org/schema/integration
    +			http://www.springframework.org/schema/integration/spring-integration.xsd">
    +
    +
    +	<!-- REQUIRED FOR TESTING -->
    +	<bridge input-channel="output"
    +			output-channel="outputTest"/>
    +
    +	<channel id="outputTest">
    +		<queue/>
    +	</channel>
    +
    +</beans:beans>

    These examples lend themselves to three scenarios:

    Scenario 1 (no input message)

    To trigger a message via the return_book_1 label, use the StubTigger interface, as +follows:

    stubFinder.trigger('return_book_1')

    To listen to the output of the message sent to output:

    Message<?> receivedMessage = messaging.receive('outputTest')

    The received message would pass the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 2 (output triggered by input)

    Since the route is set for you, you can send a message to the output +destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'input')

    To listen to the output of the message sent to output:

    Message<?> receivedMessage = messaging.receive('outputTest')

    The received message passes the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 3 (input with no output)

    Since the route is set for you, you can send a message to the input destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

    93.4 Stub Runner Stream

    Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to +integrate with Spring Stream. For the provided artifacts, it automatically downloads the +stubs and registers the required routes.

    [Warning]Warning

    If Stub Runner’s integration with Stream the messageFrom or sentTo Strings +are resolved first as a destination of a channel and no such destination exists, the +destination is resolved as a channel name.

    [Important]Important

    If you want to use Spring Cloud Stream remember, to add a dependency on +org.springframework.cloud:spring-cloud-stream-test-support.

    Maven.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-stream-test-support</artifactId>
    +    <scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile "org.springframework.cloud:spring-cloud-stream-test-support"

    +

    93.4.1 Adding the Runner to the Project

    You can have both Spring Cloud Stream and Spring Cloud Contract Stub Runner on the +classpath. Remember to annotate your test class with @AutoConfigureStubRunner.

    93.4.2 Disabling the functionality

    If you need to disable this functionality, set the stubrunner.stream.enabled=false +property.

    Assume that you have the following Maven repository with a deployed stubs for the +streamService application:

    └── .m2
    +    └── repository
    +        └── io
    +            └── codearte
    +                └── accurest
    +                    └── stubs
    +                        └── streamService
    +                            ├── 0.0.1-SNAPSHOT
    +                            │   ├── streamService-0.0.1-SNAPSHOT.pom
    +                            │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar
    +                            │   └── maven-metadata-local.xml
    +                            └── maven-metadata-local.xml

    Further assume the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── repository
    +    ├── accurest
    +    │   ├── bookDeleted.groovy
    +    │   ├── bookReturned1.groovy
    +    │   └── bookReturned2.groovy
    +    └── mappings

    Consider the following contracts (numbered 1):

    Contract.make {
    +	label 'return_book_1'
    +	input { triggeredBy('bookReturnedTriggered()') }
    +	outputMessage {
    +		sentTo('returnBook')
    +		body('''{ "bookName" : "foo" }''')
    +		headers { header('BOOK-NAME', 'foo') }
    +	}
    +}

    Now consider 2:

    Contract.make {
    +	label 'return_book_2'
    +	input {
    +		messageFrom('bookStorage')
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		messageHeaders { header('sample', 'header') }
    +	}
    +	outputMessage {
    +		sentTo('returnBook')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers { header('BOOK-NAME', 'foo') }
    +	}
    +}

    Now consider the following Spring configuration:

    stubrunner.repositoryRoot: classpath:m2repo/repository/
    +stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs
    +stubrunner.stubs-mode: remote
    +spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        output:
    +          destination: returnBook
    +        input:
    +          destination: bookStorage
    +
    +server:
    +  port: 0
    +
    +debug: true

    These examples lend themselves to three scenarios:

    Scenario 1 (no input message)

    To trigger a message via the return_book_1 label, use the StubTrigger interface as +follows:

    stubFinder.trigger('return_book_1')

    To listen to the output of the message sent to a channel whose destination is +returnBook:

    Message<?> receivedMessage = messaging.receive('returnBook')

    The received message passes the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 2 (output triggered by input)

    Since the route is set for you, you can send a message to the bookStorage +destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage')

    To listen to the output of the message sent to returnBook:

    Message<?> receivedMessage = messaging.receive('returnBook')

    The received message passes the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 3 (input with no output)

    Since the route is set for you, you can send a message to the output +destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

    93.5 Stub Runner Spring AMQP

    Spring Cloud Contract Verifier Stub Runner’s messaging module provides an easy way to +integrate with Spring AMQP’s Rabbit Template. For the provided artifacts, it +automatically downloads the stubs and registers the required routes.

    The integration tries to work standalone (that is, without interaction with a running +RabbitMQ message broker). It expects a RabbitTemplate on the application context and +uses it as a spring boot test named @SpyBean. As a result, it can use the mockito spy +functionality to verify and inspect messages sent by the application.

    On the message consumer side, the stub runner considers all @RabbitListener annotated +endpoints and all SimpleMessageListenerContainer objects on the application context.

    As messages are usually sent to exchanges in AMQP, the message contract contains the +exchange name as the destination. Message listeners on the other side are bound to +queues. Bindings connect an exchange to a queue. If message contracts are triggered, the +Spring AMQP stub runner integration looks for bindings on the application context that +match this exchange. Then it collects the queues from the Spring exchanges and tries to +find message listeners bound to these queues. The message is triggered for all matching +message listeners.

    If you need to work with routing keys, it’s enough to pass them via the amqp_receivedRoutingKey +messaging header.

    93.5.1 Adding the Runner to the Project

    You can have both Spring AMQP and Spring Cloud Contract Stub Runner on the classpath and +set the property stubrunner.amqp.enabled=true. Remember to annotate your test class +with @AutoConfigureStubRunner.

    [Important]Important

    If you already have Stream and Integration on the classpath, you need +to disable them explicitly by setting the stubrunner.stream.enabled=false and +stubrunner.integration.enabled=false properties.

    Assume that you have the following Maven repository with a deployed stubs for the +spring-cloud-contract-amqp-test application.

    └── .m2
    +    └── repository
    +        └── com
    +            └── example
    +                └── spring-cloud-contract-amqp-test
    +                    ├── 0.4.0-SNAPSHOT
    +                    │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
    +                    │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
    +                    │   └── maven-metadata-local.xml
    +                    └── maven-metadata-local.xml

    Further assume that the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── contracts
    +    └── shouldProduceValidPersonData.groovy

    Consider the following contract:

    Contract.make {
    +	// Human readable description
    +	description 'Should produce valid person data'
    +	// Label by means of which the output message can be triggered
    +	label 'contract-test.person.created.event'
    +	// input to the contract
    +	input {
    +		// the contract will be triggered by a method
    +		triggeredBy('createPerson()')
    +	}
    +	// output message of the contract
    +	outputMessage {
    +		// destination to which the output message will be sent
    +		sentTo 'contract-test.exchange'
    +		headers {
    +			header('contentType': 'application/json')
    +			header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person')
    +		}
    +		// the body of the output message
    +		body([
    +				id  : $(consumer(9), producer(regex("[0-9]+"))),
    +				name: "me"
    +		])
    +	}
    +}

    Now consider the following Spring configuration:

    stubrunner:
    +  repositoryRoot: classpath:m2repo/repository/
    +  ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs
    +  stubs-mode: remote
    +  amqp:
    +    enabled: true
    +server:
    +  port: 0

    Triggering the message

    To trigger a message using the contract above, use the StubTrigger interface as +follows:

    stubTrigger.trigger("contract-test.person.created.event")

    The message has a destination of contract-test.exchange, so the Spring AMQP stub runner +integration looks for bindings related to this exchange.

    @Bean
    +public Binding binding() {
    +	return BindingBuilder.bind(new Queue("test.queue"))
    +			.to(new DirectExchange("contract-test.exchange")).with("#");
    +}

    The binding definition binds the queue test.queue. As a result, the following listener +definition is matched and invoked with the contract message.

    @Bean
    +public SimpleMessageListenerContainer simpleMessageListenerContainer(
    +		ConnectionFactory connectionFactory,
    +		MessageListenerAdapter listenerAdapter) {
    +	SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    +	container.setConnectionFactory(connectionFactory);
    +	container.setQueueNames("test.queue");
    +	container.setMessageListener(listenerAdapter);
    +
    +	return container;
    +}

    Also, the following annotated listener matches and is invoked:

    @RabbitListener(bindings = @QueueBinding(value = @Queue("test.queue"), exchange = @Exchange(value = "contract-test.exchange", ignoreDeclarationExceptions = "true")))
    +public void handlePerson(Person person) {
    +	this.person = person;
    +}
    [Note]Note

    The message is directly handed over to the onMessage method of the +MessageListener associated with the matching SimpleMessageListenerContainer.

    Spring AMQP Test Configuration

    In order to avoid Spring AMQP trying to connect to a running broker during our tests +configure a mock ConnectionFactory.

    To disable the mocked ConnectionFactory, set the following property: +stubrunner.amqp.mockConnection=false

    stubrunner:
    +  amqp:
    +    mockConnection: false
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_troubleshooting.html b/Greenwich.SR5/multi/multi_troubleshooting.html new file mode 100644 index 00000000..30d49172 --- /dev/null +++ b/Greenwich.SR5/multi/multi_troubleshooting.html @@ -0,0 +1,8 @@ + + + 123. Troubleshooting

    123. Troubleshooting

    123.1 Log Levels

    Below are some useful loggers that contain valuable trouble shooting infomration at the DEBUG and TRACE levels.

    • org.springframework.cloud.gateway
    • org.springframework.http.server.reactive
    • org.springframework.web.reactive
    • org.springframework.boot.autoconfigure.web
    • reactor.netty
    • redisratelimiter

    123.2 Wiretap

    The Reactor Netty HttpClient and HttpServer can have wiretap enabled. When combined +with setting the reactor.netty log level to DEBUG or TRACE will enable logging of +information such as headers and bodies sent and received across the wire. To enable this, +set spring.cloud.gateway.httpserver.wiretap=true and/or +spring.cloud.gateway.httpclient.wiretap=true for the HttpServer and HttpClient +respectively.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_vault-lease-renewal.html b/Greenwich.SR5/multi/multi_vault-lease-renewal.html new file mode 100644 index 00000000..645b713f --- /dev/null +++ b/Greenwich.SR5/multi/multi_vault-lease-renewal.html @@ -0,0 +1,22 @@ + + + 109. Lease lifecycle management (renewal and revocation)

    109. Lease lifecycle management (renewal and revocation)

    With every secret, Vault creates a lease: +metadata containing information such as a time duration, +renewability, and more.

    Vault promises that the data will be valid for the given duration, +or Time To Live (TTL). Once the lease is expired, Vault can +revoke the data, and the consumer of the secret can no longer +be certain that it is valid.

    Spring Cloud Vault maintains a lease lifecycle beyond +the creation of login tokens and secrets. That said, +login tokens and secrets associated with a lease +are scheduled for renewal just before the lease expires +until terminal expiry. +Application shutdown revokes obtained login tokens and renewable +leases.

    Secret service and database backends (such as MongoDB or MySQL) +usually generate a renewable lease so generated credentials will +be disabled on application shutdown.

    [Note]Note

    Static tokens are not renewed or revoked.

    Lease renewal and revocation is enabled by default and can +be disabled by setting spring.cloud.vault.config.lifecycle.enabled +to false. This is not recommended as leases can expire and +Spring Cloud Vault cannot longer access Vault or services +using generated credentials and valid credentials remain active +after application shutdown.

    spring.cloud.vault:
    +    config.lifecycle.enabled: true

    See also: Vault Documentation: Lease, Renew, and Revoke

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_vault.config.authentication.html b/Greenwich.SR5/multi/multi_vault.config.authentication.html new file mode 100644 index 00000000..3b85ef5c --- /dev/null +++ b/Greenwich.SR5/multi/multi_vault.config.authentication.html @@ -0,0 +1,199 @@ + + + 102. Authentication methods

    102. Authentication methods

    Different organizations have different requirements for security +and authentication. Vault reflects that need by shipping multiple authentication +methods. Spring Cloud Vault supports token and AppId authentication.

    102.1 Token authentication

    Tokens are the core method for authentication within Vault. +Token authentication requires a static token to be provided using the +Bootstrap Application Context.

    [Note]Note

    Token authentication is the default authentication method. +If a token is disclosed an unintended party gains access to Vault and +can access secrets for the intended client.

    Example 102.1. bootstrap.yml

    spring.cloud.vault:
    +    authentication: TOKEN
    +    token: 00000000-0000-0000-0000-000000000000

    • authentication setting this value to TOKEN selects the Token +authentication method
    • token sets the static token to use

    See also: Vault Documentation: Tokens

    102.2 AppId authentication

    Vault supports AppId +authentication that consists of two hard to guess tokens. The AppId +defaults to spring.application.name that is statically configured. +The second token is the UserId which is a part determined by the application, +usually related to the runtime environment. IP address, Mac address or a +Docker container name are good examples. Spring Cloud Vault Config supports +IP address, Mac address and static UserId’s (e.g. supplied via System properties). +The IP and Mac address are represented as Hex-encoded SHA256 hash.

    IP address-based UserId’s use the local host’s IP address.

    Example 102.2. bootstrap.yml using SHA256 IP-Address UserId’s

    spring.cloud.vault:
    +    authentication: APPID
    +    app-id:
    +        user-id: IP_ADDRESS

    • authentication setting this value to APPID selects the AppId +authentication method
    • app-id-path sets the path of the AppId mount to use
    • user-id sets the UserId method. Possible values are IP_ADDRESS, +MAC_ADDRESS or a class name implementing a custom AppIdUserIdMechanism

    The corresponding command to generate the IP address UserId from a command line is:

    $ echo -n 192.168.99.1 | sha256sum
    [Note]Note

    Including the line break of echo leads to a different hash value +so make sure to include the -n flag.

    Mac address-based UserId’s obtain their network device from the +localhost-bound device. The configuration also allows specifying +a network-interface hint to pick the right device. The value of +network-interface is optional and can be either an interface +name or interface index (0-based).

    Example 102.3. bootstrap.yml using SHA256 Mac-Address UserId’s

    spring.cloud.vault:
    +    authentication: APPID
    +    app-id:
    +        user-id: MAC_ADDRESS
    +        network-interface: eth0

    • network-interface sets network interface to obtain the physical address

    The corresponding command to generate the IP address UserId from a command line is:

    $ echo -n 0AFEDE1234AC | sha256sum
    [Note]Note

    The Mac address is specified uppercase and without colons. +Including the line break of echo leads to a different hash value +so make sure to include the -n flag.

    102.2.1 Custom UserId

    The UserId generation is an open mechanism. You can set +spring.cloud.vault.app-id.user-id to any string and the configured +value will be used as static UserId.

    A more advanced approach lets you set spring.cloud.vault.app-id.user-id to a +classname. This class must be on your classpath and must implement +the org.springframework.cloud.vault.AppIdUserIdMechanism interface +and the createUserId method. Spring Cloud Vault will obtain the UserId +by calling createUserId each time it authenticates using AppId to +obtain a token.

    Example 102.4. bootstrap.yml

    spring.cloud.vault:
    +    authentication: APPID
    +    app-id:
    +        user-id: com.examlple.MyUserIdMechanism

    Example 102.5. MyUserIdMechanism.java

    public class MyUserIdMechanism implements AppIdUserIdMechanism {
    +
    +  @Override
    +  public String createUserId() {
    +    String userId = ...
    +    return userId;
    +  }
    +}

    See also: Vault Documentation: Using the App ID auth backend

    102.3 AppRole authentication

    AppRole is intended for machine +authentication, like the deprecated (since Vault 0.6.1) Section 102.2, “AppId authentication”. +AppRole authentication consists of two hard to guess (secret) tokens: RoleId and SecretId.

    Spring Vault supports various AppRole scenarios (push/pull mode and wrapped).

    RoleId and optionally SecretId must be provided by configuration, +Spring Vault will not look up these or create a custom SecretId.

    Example 102.6. bootstrap.yml with AppRole authentication properties

    spring.cloud.vault:
    +    authentication: APPROLE
    +    app-role:
    +        role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52

    The following scenarios are supported along the required configuration details:

    Table 102.1. Configuration

    Method

    RoleId

    SecretId

    RoleName

    Token

    Provided RoleId/SecretId

    Provided

    Provided

      

    Provided RoleId without SecretId

    Provided

       

    Provided RoleId, Pull SecretId

    Provided

    Provided

    Provided

    Provided

    Pull RoleId, provided SecretId

     

    Provided

    Provided

    Provided

    Full Pull Mode

      

    Provided

    Provided

    Wrapped

       

    Provided

    Wrapped RoleId, provided SecretId

    Provided

      

    Provided

    Provided RoleId, wrapped SecretId

     

    Provided

     

    Provided


    Table 102.2. Pull/Push/Wrapped Matrix

    RoleId

    SecretId

    Supported

    Provided

    Provided

    Provided

    Pull

    Provided

    Wrapped

    Provided

    Absent

    Pull

    Provided

    Pull

    Pull

    Pull

    Wrapped

    Pull

    Absent

    Wrapped

    Provided

    Wrapped

    Pull

    Wrapped

    Wrapped

    Wrapped

    Absent


    [Note]Note

    You can use still all combinations of push/pull/wrapped modes by providing a configured AppRoleAuthentication bean within the bootstrap context. Spring Cloud Vault cannot derive all possible AppRole combinations from the configuration properties.

    [Important]Important

    AppRole authentication is limited to simple pull mode using reactive infrastructure. Full pull mode is not yet supported. Using Spring Cloud Vault with the Spring WebFlux stack enables Vault’s reactive auto-configuration which can be disabled by setting spring.cloud.vault.reactive.enabled=false.

    Example 102.7. bootstrap.yml with all AppRole authentication properties

    spring.cloud.vault:
    +    authentication: APPROLE
    +    app-role:
    +        role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52
    +        secret-id: 1696536f-1976-73b1-b241-0b4213908d39
    +        role: my-role
    +        app-role-path: approle

    • role-id sets the RoleId.
    • secret-id sets the SecretId. SecretId can be omitted if AppRole is configured without requiring SecretId (See bind_secret_id).
    • role: sets the AppRole name for pull mode.
    • app-role-path sets the path of the approle authentication mount to use.

    See also: Vault Documentation: Using the AppRole auth backend

    102.4 AWS-EC2 authentication

    The aws-ec2 +auth backend provides a secure introduction mechanism +for AWS EC2 instances, allowing automated retrieval of a Vault +token. Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats AWS as a Trusted Third Party and uses the +cryptographically signed dynamic metadata information that uniquely +represents each EC2 instance.

    Example 102.8. bootstrap.yml using AWS-EC2 Authentication

    spring.cloud.vault:
    +    authentication: AWS_EC2

    AWS-EC2 authentication enables nonce by default to follow +the Trust On First Use (TOFU) principle. Any unintended party that +gains access to the PKCS#7 identity metadata can authenticate +against Vault.

    During the first login, Spring Cloud Vault generates a nonce +that is stored in the auth backend aside the instance Id. +Re-authentication requires the same nonce to be sent. Any other +party does not have the nonce and can raise an alert in Vault for +further investigation.

    The nonce is kept in memory and is lost during application restart. +You can configure a static nonce with spring.cloud.vault.aws-ec2.nonce.

    AWS-EC2 authentication roles are optional and default to the AMI. +You can configure the authentication role by setting the +spring.cloud.vault.aws-ec2.role property.

    Example 102.9. bootstrap.yml with configured role

    spring.cloud.vault:
    +    authentication: AWS_EC2
    +    aws-ec2:
    +        role: application-server

    Example 102.10. bootstrap.yml with all AWS EC2 authentication properties

    spring.cloud.vault:
    +    authentication: AWS_EC2
    +    aws-ec2:
    +        role: application-server
    +        aws-ec2-path: aws-ec2
    +        identity-document: http://...
    +        nonce: my-static-nonce

    • authentication setting this value to AWS_EC2 selects the AWS EC2 +authentication method
    • role sets the name of the role against which the login is being attempted.
    • aws-ec2-path sets the path of the AWS EC2 mount to use
    • identity-document sets URL of the PKCS#7 AWS EC2 identity document
    • nonce used for AWS-EC2 authentication. An empty nonce defaults to nonce generation

    See also: Vault Documentation: Using the aws auth backend

    102.5 AWS-IAM authentication

    The aws backend provides a secure +authentication mechanism for AWS IAM roles, allowing the automatic authentication with +vault based on the current IAM role of the running application. + Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats AWS as a Trusted Third Party and uses the +4 pieces of information signed by the caller with their IAM credentials + to verify that the caller is indeed using that IAM role.

    The current IAM role the application is running in is automatically calculated. +If you are running your application on AWS ECS then the application +will use the IAM role assigned to the ECS task of the running container. +If you are running your application naked on top of an EC2 instance then +the IAM role used will be the one assigned to the EC2 instance.

    When using the AWS-IAM authentication you must create a role in Vault +and assign it to your IAM role. An empty role defaults to +the friendly name the current IAM role.

    Example 102.11. bootstrap.yml with required AWS-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: AWS_IAM

    Example 102.12. bootstrap.yml with all AWS-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: AWS_IAM
    +    aws-iam:
    +        role: my-dev-role
    +        aws-path: aws
    +        server-id: some.server.name

    • role sets the name of the role against which the login is being attempted. This should be bound to your IAM role. If one is not supplied then the friendly name of the current IAM user will be used as the vault role.
    • aws-path sets the path of the AWS mount to use
    • server-id sets the value to use for the X-Vault-AWS-IAM-Server-ID header preventing certain types of replay attacks.

    AWS-IAM requires the AWS Java SDK dependency (com.amazonaws:aws-java-sdk-core) +as the authentication implementation uses AWS SDK types for credentials and request signing.

    See also: Vault Documentation: Using the aws auth backend

    102.6 Azure MSI authentication

    The azure +auth backend provides a secure introduction mechanism +for Azure VM instances, allowing automated retrieval of a Vault +token. Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats Azure as a Trusted Third Party and uses the +managed service identity and instance metadata information that can be +bound to a VM instance.

    Example 102.13. bootstrap.yml with required Azure Authentication properties

    spring.cloud.vault:
    +    authentication: AZURE_MSI
    +    azure-msi:
    +        role: my-dev-role

    Example 102.14. bootstrap.yml with all Azure Authentication properties

    spring.cloud.vault:
    +    authentication: AZURE_MSI
    +    azure-msi:
    +        role: my-dev-role
    +        azure-path: azure

    • role sets the name of the role against which the login is being attempted.
    • azure-path sets the path of the Azure mount to use

    Azure MSI authentication fetches environmental details about the virtual machine +(subscription Id, resource group, VM name) from the instance metadata service.

    See also: Vault Documentation: Using the azure auth backend

    102.7 TLS certificate authentication

    The cert auth backend allows authentication using SSL/TLS client +certificates that are either signed by a CA or self-signed.

    To enable cert authentication you need to:

    1. Use SSL, see Chapter 108, Vault Client SSL configuration
    2. Configure a Java Keystore that contains the client +certificate and the private key
    3. Set the spring.cloud.vault.authentication to CERT

    Example 102.15. bootstrap.yml

    spring.cloud.vault:
    +    authentication: CERT
    +    ssl:
    +        key-store: classpath:keystore.jks
    +        key-store-password: changeit
    +        cert-auth-path: cert

    See also: Vault Documentation: Using the Cert auth backend

    102.8 Cubbyhole authentication

    Cubbyhole authentication uses Vault primitives to provide a secured authentication +workflow. Cubbyhole authentication uses tokens as primary login method. +An ephemeral token is used to obtain a second, login VaultToken from Vault’s +Cubbyhole secret backend. The login token is usually longer-lived and used to +interact with Vault. The login token will be retrieved from a wrapped +response stored at /cubbyhole/response.

    Creating a wrapped token

    [Note]Note

    Response Wrapping for token creation requires Vault 0.6.0 or higher.

    Example 102.16. Creating and storing tokens

    $ vault token-create -wrap-ttl="10m"
    +Key                            Value
    +---                            -----
    +wrapping_token:                397ccb93-ff6c-b17b-9389-380b01ca2645
    +wrapping_token_ttl:            0h10m0s
    +wrapping_token_creation_time:  2016-09-18 20:29:48.652957077 +0200 CEST
    +wrapped_accessor:              46b6aebb-187f-932a-26d7-4f3d86a68319

    Example 102.17. bootstrap.yml

    spring.cloud.vault:
    +    authentication: CUBBYHOLE
    +    token: 397ccb93-ff6c-b17b-9389-380b01ca2645

    See also:

    102.9 GCP-GCE authentication

    The gcp +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials.

    GCP GCE (Google Compute Engine) authentication creates a signature in the form of a +JSON Web Token (JWT) for a service account. A JWT for a Compute Engine instance +is obtained from the GCE metadata service using Instance identification. +This API creates a JSON Web Token that can be used to confirm the instance identity.

    Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats GCP as a Trusted Third Party and uses the +cryptographically signed dynamic metadata information that uniquely +represents each GCP service account.

    Example 102.18. bootstrap.yml with required GCP-GCE Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_GCE
    +    gcp-gce:
    +        role: my-dev-role

    Example 102.19. bootstrap.yml with all GCP-GCE Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_GCE
    +    gcp-gce:
    +        gcp-path: gcp
    +        role: my-dev-role
    +        service-account: my-service@projectid.iam.gserviceaccount.com

    • role sets the name of the role against which the login is being attempted.
    • gcp-path sets the path of the GCP mount to use
    • service-account allows overriding the service account Id to a specific value. Defaults to the default service account.

    See also:

    102.10 GCP-IAM authentication

    The gcp +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials.

    GCP IAM authentication creates a signature in the form of a JSON Web Token (JWT) +for a service account. A JWT for a service account is obtained by +calling GCP IAM’s projects.serviceAccounts.signJwt API. The caller authenticates against GCP IAM +and proves thereby its identity. This Vault backend treats GCP as a Trusted Third Party.

    IAM credentials can be obtained from either the runtime environment +, specifically the GOOGLE_APPLICATION_CREDENTIALS +environment variable, the Google Compute metadata service, +or supplied externally as e.g. JSON or base64 encoded. +JSON is the preferred form as it carries the project id and +service account identifier required for calling projects.serviceAccounts.signJwt.

    Example 102.20. bootstrap.yml with required GCP-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_IAM
    +    gcp-iam:
    +        role: my-dev-role

    Example 102.21. bootstrap.yml with all GCP-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_IAM
    +    gcp-iam:
    +        credentials:
    +            location: classpath:credentials.json
    +            encoded-key: e+KApn0=
    +        gcp-path: gcp
    +        jwt-validity: 15m
    +        project-id: my-project-id
    +        role: my-dev-role
    +        service-account-id: my-service@projectid.iam.gserviceaccount.com

    • role sets the name of the role against which the login is being attempted.
    • credentials.location path to the credentials resource that contains Google credentials in JSON format.
    • credentials.encoded-key the base64 encoded contents of an OAuth2 account private key in the JSON format.
    • gcp-path sets the path of the GCP mount to use
    • jwt-validity configures the JWT token validity. Defaults to 15 minutes.
    • project-id allows overriding the project Id to a specific value. Defaults to the project Id from the obtained credential.
    • service-account allows overriding the service account Id to a specific value. Defaults to the service account from the obtained credential.

    GCP IAM authentication requires the Google Cloud Java SDK dependency +(com.google.apis:google-api-services-iam and com.google.auth:google-auth-library-oauth2-http) +as the authentication implementation uses Google APIs for credentials and JWT signing.

    [Note]Note

    Google credentials require an OAuth 2 token maintaining the token lifecycle. All API +is synchronous therefore, GcpIamAuthentication does not support AuthenticationSteps which is +required for reactive usage.

    See also:

    102.11 Kubernetes authentication

    Kubernetes authentication mechanism (since Vault 0.8.3) allows to authenticate with Vault using a Kubernetes Service Account Token. +The authentication is role based and the role is bound to a service account name and a namespace.

    A file containing a JWT token for a pod’s service account is automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

    Example 102.22. bootstrap.yml with all Kubernetes authentication properties

    spring.cloud.vault:
    +    authentication: KUBERNETES
    +    kubernetes:
    +        role: my-dev-role
    +        kubernetes-path: kubernetes
    +        service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token

    • role sets the Role.
    • kubernetes-path sets the path of the Kubernetes mount to use.
    • service-account-token-file sets the location of the file containing the Kubernetes Service Account Token. Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token.

    See also:

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_vault.config.backends.configurer.html b/Greenwich.SR5/multi/multi_vault.config.backends.configurer.html new file mode 100644 index 00000000..4a4deec1 --- /dev/null +++ b/Greenwich.SR5/multi/multi_vault.config.backends.configurer.html @@ -0,0 +1,22 @@ + + + 105. Configure PropertySourceLocator behavior

    105. Configure PropertySourceLocator behavior

    Spring Cloud Vault uses property-based configuration to create PropertySources +for generic and discovered secret backends.

    Discovered backends provide VaultSecretBackendDescriptor beans to describe the configuration +state to use secret backend as PropertySource. A SecretBackendMetadataFactory is required +to create a SecretBackendMetadata object which contains path, name and property transformation +configuration.

    SecretBackendMetadata is used to back a particular PropertySource.

    You can register an arbitrary number of beans implementing VaultConfigurer for customization. +Default generic and discovered backend registration is disabled if Spring Cloud Vault discovers +at least one VaultConfigurer bean. You can however enable default registration with +SecretBackendConfigurer.registerDefaultGenericSecretBackends() and SecretBackendConfigurer.registerDefaultDiscoveredSecretBackends().

    public class CustomizationBean implements VaultConfigurer {
    +
    +    @Override
    +    public void addSecretBackends(SecretBackendConfigurer configurer) {
    +
    +        configurer.add("secret/my-application");
    +
    +        configurer.registerDefaultGenericSecretBackends(false);
    +        configurer.registerDefaultDiscoveredSecretBackends(true);
    +    }
    +}
    [Note]Note

    All customization is required to happen in the bootstrap context. Add your configuration +classes to META-INF/spring.factories at org.springframework.cloud.bootstrap.BootstrapConfiguration +in your application.

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_vault.config.backends.database-backends.html b/Greenwich.SR5/multi/multi_vault.config.backends.database-backends.html new file mode 100644 index 00000000..e81ea7d0 --- /dev/null +++ b/Greenwich.SR5/multi/multi_vault.config.backends.database-backends.html @@ -0,0 +1,103 @@ + + + 104. Database backends

    104. Database backends

    Vault supports several database secret backends to generate database +credentials dynamically based on configured roles. This means +services that need to access a database no longer need to configure +credentials: they can request them from Vault, and use Vault’s leasing +mechanism to more easily roll keys.

    Spring Cloud Vault integrates with these backends:

    Using a database secret backend requires to enable the +backend in the configuration and the spring-cloud-vault-config-databases +dependency.

    Vault ships since 0.7.1 with a dedicated database secret backend that allows +database integration via plugins. You can use that specific backend by using the +generic database backend. Make sure to specify the appropriate +backend path, e.g. spring.cloud.vault.mysql.role.backend=database.

    Example 104.1. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-databases</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    [Note]Note

    Enabling multiple JDBC-compliant databases will generate credentials +and store them by default in the same property keys hence property names for +JDBC secrets need to be configured separately.

    104.1 Database

    Spring Cloud Vault can obtain credentials for any database listed at +https://www.vaultproject.io/api/secret/databases/index.html. +The integration can be enabled by setting +spring.cloud.vault.database.enabled=true (default false) and +providing the role name with spring.cloud.vault.database.role=….

    While the database backend is a generic one, spring.cloud.vault.database +specifically targets JDBC databases. Username and password are +stored in spring.datasource.username and spring.datasource.password +so using Spring Boot will pick up the generated credentials +for your DataSource without further configuration. +You can configure the property names by setting +spring.cloud.vault.database.username-property and +spring.cloud.vault.database.password-property.

    spring.cloud.vault:
    +    database:
    +        enabled: true
    +        role: readonly
    +        backend: database
    +        username-property: spring.datasource.username
    +        password-property: spring.datasource.password
    • enabled setting this value to true enables the Database backend config usage
    • role sets the role name of the Database role definition
    • backend sets the path of the Database mount to use
    • username-property sets the property name in which the Database username is stored
    • password-property sets the property name in which the Database password is stored

    See also: Vault Documentation: Database Secrets backend

    [Warning]Warning

    Spring Cloud Vault does not support getting new credentials and +configuring your DataSource with them when the maximum lease time +has been reached. That is, if max_ttl of the Database role in Vault +is set to 24h that means that 24 hours after your application has +started it can no longer authenticate with the database.

    104.2 Apache Cassandra

    [Note]Note

    The cassandra backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as cassandra.

    Spring Cloud Vault can obtain credentials for Apache Cassandra. +The integration can be enabled by setting +spring.cloud.vault.cassandra.enabled=true (default false) and +providing the role name with spring.cloud.vault.cassandra.role=….

    Username and password are stored in spring.data.cassandra.username +and spring.data.cassandra.password so using Spring Boot will pick +up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.cassandra.username-property and +spring.cloud.vault.cassandra.password-property.

    spring.cloud.vault:
    +    cassandra:
    +        enabled: true
    +        role: readonly
    +        backend: cassandra
    +        username-property: spring.data.cassandra.username
    +        password-property: spring.data.cassandra.password
    • enabled setting this value to true enables the Cassandra backend config usage
    • role sets the role name of the Cassandra role definition
    • backend sets the path of the Cassandra mount to use
    • username-property sets the property name in which the Cassandra username is stored
    • password-property sets the property name in which the Cassandra password is stored

    See also: Vault Documentation: Setting up Apache Cassandra with Vault

    104.3 MongoDB

    [Note]Note

    The mongodb backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as mongodb.

    Spring Cloud Vault can obtain credentials for MongoDB. +The integration can be enabled by setting +spring.cloud.vault.mongodb.enabled=true (default false) and +providing the role name with spring.cloud.vault.mongodb.role=….

    Username and password are stored in spring.data.mongodb.username +and spring.data.mongodb.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.mongodb.username-property and +spring.cloud.vault.mongodb.password-property.

    spring.cloud.vault:
    +    mongodb:
    +        enabled: true
    +        role: readonly
    +        backend: mongodb
    +        username-property: spring.data.mongodb.username
    +        password-property: spring.data.mongodb.password
    • enabled setting this value to true enables the MongodB backend config usage
    • role sets the role name of the MongoDB role definition
    • backend sets the path of the MongoDB mount to use
    • username-property sets the property name in which the MongoDB username is stored
    • password-property sets the property name in which the MongoDB password is stored

    See also: Vault Documentation: Setting up MongoDB with Vault

    104.4 MySQL

    [Note]Note

    The mysql backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as mysql. +Configuration for spring.cloud.vault.mysql will be removed in a future version.

    Spring Cloud Vault can obtain credentials for MySQL. +The integration can be enabled by setting +spring.cloud.vault.mysql.enabled=true (default false) and +providing the role name with spring.cloud.vault.mysql.role=….

    Username and password are stored in spring.datasource.username +and spring.datasource.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.mysql.username-property and +spring.cloud.vault.mysql.password-property.

    spring.cloud.vault:
    +    mysql:
    +        enabled: true
    +        role: readonly
    +        backend: mysql
    +        username-property: spring.datasource.username
    +        password-property: spring.datasource.password
    • enabled setting this value to true enables the MySQL backend config usage
    • role sets the role name of the MySQL role definition
    • backend sets the path of the MySQL mount to use
    • username-property sets the property name in which the MySQL username is stored
    • password-property sets the property name in which the MySQL password is stored

    See also: Vault Documentation: Setting up MySQL with Vault

    104.5 PostgreSQL

    [Note]Note

    The postgresql backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as postgresql. +Configuration for spring.cloud.vault.postgresql will be removed in a future version.

    Spring Cloud Vault can obtain credentials for PostgreSQL. +The integration can be enabled by setting +spring.cloud.vault.postgresql.enabled=true (default false) and +providing the role name with spring.cloud.vault.postgresql.role=….

    Username and password are stored in spring.datasource.username +and spring.datasource.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.postgresql.username-property and +spring.cloud.vault.postgresql.password-property.

    spring.cloud.vault:
    +    postgresql:
    +        enabled: true
    +        role: readonly
    +        backend: postgresql
    +        username-property: spring.datasource.username
    +        password-property: spring.datasource.password
    • enabled setting this value to true enables the PostgreSQL backend config usage
    • role sets the role name of the PostgreSQL role definition
    • backend sets the path of the PostgreSQL mount to use
    • username-property sets the property name in which the PostgreSQL username is stored
    • password-property sets the property name in which the PostgreSQL password is stored

    See also: Vault Documentation: Setting up PostgreSQL with Vault

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_vault.config.backends.html b/Greenwich.SR5/multi/multi_vault.config.backends.html new file mode 100644 index 00000000..6329ef3c --- /dev/null +++ b/Greenwich.SR5/multi/multi_vault.config.backends.html @@ -0,0 +1,99 @@ + + + 103. Secret Backends

    103. Secret Backends

    103.1 Generic Backend

    Spring Cloud Vault supports at the basic level the generic secret +backend. The generic secret backend allows storage of arbitrary +values as key-value store. A single context can store one or many +key-value tuples. Contexts can be organized hierarchically. +Spring Cloud Vault allows using the Application name +and a default context name (application) in combination with active +profiles.

    /secret/{application}/{profile}
    +/secret/{application}
    +/secret/{default-context}/{profile}
    +/secret/{default-context}

    The application name is determined by the properties:

    • spring.cloud.vault.generic.application-name
    • spring.cloud.vault.application-name
    • spring.application.name

    Secrets can be obtained from other contexts within the generic backend by adding their +paths to the application name, separated by commas. For example, given the application +name usefulapp,mysql1,projectx/aws, each of these folders will be used:

    • /secret/usefulapp
    • /secret/mysql1
    • /secret/projectx/aws

    Spring Cloud Vault adds all active profiles to the list of possible context paths. +No active profiles will skip accessing contexts with a profile name.

    Properties are exposed like they are stored (i.e. without additional prefixes).

    spring.cloud.vault:
    +    generic:
    +        enabled: true
    +        backend: secret
    +        profile-separator: '/'
    +        default-context: application
    +        application-name: my-app
    • enabled setting this value to false disables the secret backend +config usage
    • backend sets the path of the secret mount to use
    • default-context sets the context name used by all applications
    • application-name overrides the application name for use in the generic backend
    • profile-separator separates the profile name from the context in +property sources with profiles
    [Note]Note

    The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable generic secret backend usage for non-versioned key-value backends and kv secret backend usage for versioned key-value backends.

    See also: Vault Documentation: Using the KV Secrets Engine - Version 1 (generic secret backend)

    103.2 Versioned Key-Value Backend

    Spring Cloud Vault supports the versioned Key-Value secret +backend. The key-value backend allows storage of arbitrary +values as key-value store. A single context can store one or many +key-value tuples. Contexts can be organized hierarchically. +Spring Cloud Vault allows using the Application name +and a default context name (application) in combination with active +profiles.

    /secret/{application}/{profile}
    +/secret/{application}
    +/secret/{default-context}/{profile}
    +/secret/{default-context}

    The application name is determined by the properties:

    • spring.cloud.vault.kv.application-name
    • spring.cloud.vault.application-name
    • spring.application.name

    Secrets can be obtained from other contexts within the key-value backend by adding their +paths to the application name, separated by commas. For example, given the application +name usefulapp,mysql1,projectx/aws, each of these folders will be used:

    • /secret/usefulapp
    • /secret/mysql1
    • /secret/projectx/aws

    Spring Cloud Vault adds all active profiles to the list of possible context paths. +No active profiles will skip accessing contexts with a profile name.

    Properties are exposed like they are stored (i.e. without additional prefixes).

    [Note]Note

    Spring Cloud Vault adds the data/ context between the mount path and the actual context path.

    spring.cloud.vault:
    +    kv:
    +        enabled: true
    +        backend: secret
    +        profile-separator: '/'
    +        default-context: application
    +        application-name: my-app
    • enabled setting this value to false disables the secret backend +config usage
    • backend sets the path of the secret mount to use
    • default-context sets the context name used by all applications
    • application-name overrides the application name for use in the generic backend
    • profile-separator separates the profile name from the context in +property sources with profiles
    [Note]Note

    The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable generic secret backend usage for non-versioned key-value backends and kv secret backend usage for versioned key-value backends.

    See also: Vault Documentation: Using the KV Secrets Engine - Version 2 (versioned key-value backend)

    103.3 Consul

    Spring Cloud Vault can obtain credentials for HashiCorp Consul. +The Consul integration requires the spring-cloud-vault-config-consul +dependency.

    Example 103.1. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-consul</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    The integration can be enabled by setting +spring.cloud.vault.consul.enabled=true (default false) and +providing the role name with spring.cloud.vault.consul.role=….

    The obtained token is stored in spring.cloud.consul.token +so using Spring Cloud Consul can pick up the generated +credentials without further configuration. You can configure +the property name by setting spring.cloud.vault.consul.token-property.

    spring.cloud.vault:
    +    consul:
    +        enabled: true
    +        role: readonly
    +        backend: consul
    +        token-property: spring.cloud.consul.token
    • enabled setting this value to true enables the Consul backend config usage
    • role sets the role name of the Consul role definition
    • backend sets the path of the Consul mount to use
    • token-property sets the property name in which the Consul ACL token is stored

    See also: Vault Documentation: Setting up Consul with Vault

    103.4 RabbitMQ

    Spring Cloud Vault can obtain credentials for RabbitMQ.

    The RabbitMQ integration requires the spring-cloud-vault-config-rabbitmq +dependency.

    Example 103.2. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-rabbitmq</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    The integration can be enabled by setting +spring.cloud.vault.rabbitmq.enabled=true (default false) +and providing the role name with spring.cloud.vault.rabbitmq.role=….

    Username and password are stored in spring.rabbitmq.username +and spring.rabbitmq.password so using Spring Boot will pick up the generated +credentials without further configuration. You can configure the property names +by setting spring.cloud.vault.rabbitmq.username-property and +spring.cloud.vault.rabbitmq.password-property.

    spring.cloud.vault:
    +    rabbitmq:
    +        enabled: true
    +        role: readonly
    +        backend: rabbitmq
    +        username-property: spring.rabbitmq.username
    +        password-property: spring.rabbitmq.password
    • enabled setting this value to true enables the RabbitMQ backend config usage
    • role sets the role name of the RabbitMQ role definition
    • backend sets the path of the RabbitMQ mount to use
    • username-property sets the property name in which the RabbitMQ username is stored
    • password-property sets the property name in which the RabbitMQ password is stored

    See also: Vault Documentation: Setting up RabbitMQ with Vault

    103.5 AWS

    Spring Cloud Vault can obtain credentials for AWS.

    The AWS integration requires the spring-cloud-vault-config-aws +dependency.

    Example 103.3. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-aws</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    The integration can be enabled by setting +spring.cloud.vault.aws=true (default false) +and providing the role name with spring.cloud.vault.aws.role=….

    The access key and secret key are stored in cloud.aws.credentials.accessKey +and cloud.aws.credentials.secretKey so using Spring Cloud AWS will pick up the generated +credentials without further configuration. You can configure the property names +by setting spring.cloud.vault.aws.access-key-property and +spring.cloud.vault.aws.secret-key-property.

    spring.cloud.vault:
    +    aws:
    +        enabled: true
    +        role: readonly
    +        backend: aws
    +        access-key-property: cloud.aws.credentials.accessKey
    +        secret-key-property: cloud.aws.credentials.secretKey
    • enabled setting this value to true enables the AWS backend config usage
    • role sets the role name of the AWS role definition
    • backend sets the path of the AWS mount to use
    • access-key-property sets the property name in which the AWS access key is stored
    • secret-key-property sets the property name in which the AWS secret key is stored

    See also: Vault Documentation: Setting up AWS with Vault

    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_vault.config.fail-fast.html b/Greenwich.SR5/multi/multi_vault.config.fail-fast.html new file mode 100644 index 00000000..f7fe05b7 --- /dev/null +++ b/Greenwich.SR5/multi/multi_vault.config.fail-fast.html @@ -0,0 +1,8 @@ + + + 107. Vault Client Fail Fast

    107. Vault Client Fail Fast

    In some cases, it may be desirable to fail startup of a service if +it cannot connect to the Vault Server. If this is the desired +behavior, set the bootstrap configuration property +spring.cloud.vault.fail-fast=true and the client will halt with +an Exception.

    spring.cloud.vault:
    +    fail-fast: true
    \ No newline at end of file diff --git a/Greenwich.SR5/multi/multi_vault.config.ssl.html b/Greenwich.SR5/multi/multi_vault.config.ssl.html new file mode 100644 index 00000000..1218e1ce --- /dev/null +++ b/Greenwich.SR5/multi/multi_vault.config.ssl.html @@ -0,0 +1,13 @@ + + + 108. Vault Client SSL configuration

    108. Vault Client SSL configuration

    SSL can be configured declaratively by setting various properties. +You can set either javax.net.ssl.trustStore to configure +JVM-wide SSL settings or spring.cloud.vault.ssl.trust-store +to set SSL settings only for Spring Cloud Vault Config.

    spring.cloud.vault:
    +    ssl:
    +        trust-store: classpath:keystore.jks
    +        trust-store-password: changeit
    • trust-store sets the resource for the trust-store. SSL-secured Vault +communication will validate the Vault SSL certificate with the specified +trust-store.
    • trust-store-password sets the trust-store password

    Please note that configuring spring.cloud.vault.ssl.* can be only +applied when either Apache Http Components or the OkHttp client +is on your class-path.

    \ No newline at end of file diff --git a/Greenwich.SR5/single/css/highlight.css b/Greenwich.SR5/single/css/highlight.css new file mode 100644 index 00000000..3850f8b9 --- /dev/null +++ b/Greenwich.SR5/single/css/highlight.css @@ -0,0 +1,35 @@ +/* + code highlight CSS resemblign the Eclipse IDE default color schema + @author Costin Leau +*/ + +.hl-keyword { + color: #7F0055; + font-weight: bold; +} + +.hl-comment { + color: #3F5F5F; + font-style: italic; +} + +.hl-multiline-comment { + color: #3F5FBF; + font-style: italic; +} + +.hl-tag { + color: #3F7F7F; +} + +.hl-attribute { + color: #7F007F; +} + +.hl-value { + color: #2A00FF; +} + +.hl-string { + color: #2A00FF; +} \ No newline at end of file diff --git a/Greenwich.SR5/single/css/manual-multipage.css b/Greenwich.SR5/single/css/manual-multipage.css new file mode 100644 index 00000000..b790654b --- /dev/null +++ b/Greenwich.SR5/single/css/manual-multipage.css @@ -0,0 +1,9 @@ +@IMPORT url("manual.css"); + +body.firstpage { + background: url("../images/background.png") no-repeat center top; +} + +div.part h1 { + border-top: none; +} diff --git a/Greenwich.SR5/single/css/manual-singlepage.css b/Greenwich.SR5/single/css/manual-singlepage.css new file mode 100644 index 00000000..303192a8 --- /dev/null +++ b/Greenwich.SR5/single/css/manual-singlepage.css @@ -0,0 +1,6 @@ +@IMPORT url("manual.css"); + +body { + background: url("../images/background.png") no-repeat center top; +} + diff --git a/Greenwich.SR5/single/css/manual.css b/Greenwich.SR5/single/css/manual.css new file mode 100644 index 00000000..20cf07da --- /dev/null +++ b/Greenwich.SR5/single/css/manual.css @@ -0,0 +1,342 @@ +@IMPORT url("highlight.css"); + +html { + padding: 0pt; + margin: 0pt; +} + +body { + color: #333333; + margin: 15px 30px; + font-family: Helvetica, Arial, Freesans, Clean, Sans-serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +code { + font-size: 16px; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +:not(a) > code { + color: #6D180B; +} + +:not(pre) > code { + background-color: #F2F2F2; + border: 1px solid #CCCCCC; + border-radius: 4px; + padding: 1px 3px 0; + text-shadow: none; + white-space: nowrap; +} + +body > *:first-child { + margin-top: 0 !important; +} + +div { + margin: 0pt; +} + +hr { + border: 1px solid #CCCCCC; + background: #CCCCCC; +} + +h1, h2, h3, h4, h5, h6 { + color: #000000; + cursor: text; + font-weight: bold; + margin: 30px 0 10px; + padding: 0; +} + +h1, h2, h3 { + margin: 40px 0 10px; +} + +h1 { + margin: 70px 0 30px; + padding-top: 20px; +} + +div.part h1 { + border-top: 1px dotted #CCCCCC; +} + +h1, h1 code { + font-size: 32px; +} + +h2, h2 code { + font-size: 24px; +} + +h3, h3 code { + font-size: 20px; +} + +h4, h1 code, h5, h5 code, h6, h6 code { + font-size: 18px; +} + +div.book, div.chapter, div.appendix, div.part, div.preface { + min-width: 300px; + max-width: 1200px; + margin: 0 auto; +} + +p.releaseinfo { + font-weight: bold; + margin-bottom: 40px; + margin-top: 40px; +} + +div.authorgroup { + line-height: 1; +} + +p.copyright { + line-height: 1; + margin-bottom: -5px; +} + +.legalnotice p { + font-style: italic; + font-size: 14px; + line-height: 1; +} + +div.titlepage + p, div.titlepage + p { + margin-top: 0; +} + +pre { + line-height: 1.0; + color: black; +} + +a { + color: #4183C4; + text-decoration: none; +} + +p { + margin: 15px 0; + text-align: left; +} + +ul, ol { + padding-left: 30px; +} + +li p { + margin: 0; +} + +div.table { + margin: 1em; + padding: 0.5em; + text-align: center; +} + +div.table table, div.informaltable table { + display: table; + width: 100%; +} + +div.table td { + padding-left: 7px; + padding-right: 7px; +} + +.sidebar { + line-height: 1.4; + padding: 0 20px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; +} + +.sidebar p.title { + color: #6D180B; +} + +pre.programlisting, pre.screen { + font-size: 15px; + padding: 6px 10px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; + clear: both; + overflow: auto; + line-height: 1.4; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +table { + border-collapse: collapse; + border-spacing: 0; + border: 1px solid #DDDDDD !important; + border-radius: 4px !important; + border-collapse: separate !important; + line-height: 1.6; +} + +table thead { + background: #F5F5F5; +} + +table tr { + border: none; + border-bottom: none; +} + +table th { + font-weight: bold; +} + +table th, table td { + border: none !important; + padding: 6px 13px; +} + +table tr:nth-child(2n) { + background-color: #F8F8F8; +} + +td p { + margin: 0 0 15px 0; +} + +div.table-contents td p { + margin: 0; +} + +div.important *, div.note *, div.tip *, div.warning *, div.navheader *, div.navfooter *, div.calloutlist * { + border: none !important; + background: none !important; + margin: 0; +} + +div.important p, div.note p, div.tip p, div.warning p { + color: #6F6F6F; + line-height: 1.6; +} + +div.important code, div.note code, div.tip code, div.warning code { + background-color: #F2F2F2 !important; + border: 1px solid #CCCCCC !important; + border-radius: 4px !important; + padding: 1px 3px 0 !important; + text-shadow: none !important; + white-space: nowrap !important; +} + +.note th, .tip th, .warning th { + display: none; +} + +.note tr:first-child td, .tip tr:first-child td, .warning tr:first-child td { + border-right: 1px solid #CCCCCC !important; + padding-top: 10px; +} + +div.calloutlist p, div.calloutlist td { + padding: 0; + margin: 0; +} + +div.calloutlist > table > tbody > tr > td:first-child { + padding-left: 10px; + width: 30px !important; +} + +div.important, div.note, div.tip, div.warning { + margin-left: 0px !important; + margin-right: 20px !important; + margin-top: 20px; + margin-bottom: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +div.toc { + line-height: 1.2; +} + +dl, dt { + margin-top: 1px; + margin-bottom: 0; +} + +div.toc > dl > dt { + font-size: 32px; + font-weight: bold; + margin: 30px 0 10px 0; + display: block; +} + +div.toc > dl > dd > dl > dt { + font-size: 24px; + font-weight: bold; + margin: 20px 0 10px 0; + display: block; +} + +div.toc > dl > dd > dl > dd > dl > dt { + font-weight: bold; + font-size: 20px; + margin: 10px 0 0 0; +} + +tbody.footnotes * { + border: none !important; +} + +div.footnote p { + margin: 0; + line-height: 1; +} + +div.footnote p sup { + margin-right: 6px; + vertical-align: middle; +} + +div.navheader { + border-bottom: 1px solid #CCCCCC; +} + +div.navfooter { + border-top: 1px solid #CCCCCC; +} + +.title { + margin-left: -1em; + padding-left: 1em; +} + +.title > a { + position: absolute; + visibility: hidden; + display: block; + font-size: 0.85em; + margin-top: 0.05em; + margin-left: -1em; + vertical-align: text-top; + color: black; +} + +.title > a:before { + content: "\00A7"; +} + +.title:hover > a, .title > a:hover, .title:hover > a:hover { + visibility: visible; +} + +.title:focus > a, .title > a:focus, .title:focus > a:focus { + outline: 0; +} diff --git a/Greenwich.SR5/single/images/background.png b/Greenwich.SR5/single/images/background.png new file mode 100644 index 0000000000000000000000000000000000000000..15dca6fbe2669fae3609605e49c69cc414f1b6ed GIT binary patch literal 18255 zcmZ{Mc{tQ-|NlrKgrcaFbPBDOvWBUg7G=wtim_B8Ysgq;M%hj&Dizr#DKZMBkY&bF zQI^rsG?*CsWEtBu%$S+a=XX!f_xC*4>2RIPIp^}n{kiY^y}jPA_v?1U#_HHA$qkYS z1Y(u>@jq=52vKeDlPn+z~j!r2!xcp@J9rZ zo~ZL*W#N2~h3F^Y#kf z79Vq?HYz92POY^z60RQgu$cgc!baLFp8`pJN$ z)TpgHDYO!o(|FCbF@nU|Z4{PyQT_pWk^4ba(@3pLy~5i|7uwlU`v1B%7(o3njiTd=qKqO7b}K-at&!f*f2n8M46&RIPn?wT2jQCY?} ze6G^KcX(b!Y*uXj(zgAp+m$yS9Gsr>(+F2nC60BdVfIQ`)cSJ{^*od zepxlPa|MUm>e9Vgly6ynJN3^PvB=>&xF()rO3xDmHI z=|xsK0?M48ABv)1&|8*aUyhO2#E8jlc2-#f51xWHc^hUwi&%dc@+wWVCpXJq!}S%S zg>L#^WBV(Qw|v9bo1MW5gc=&srYW_5F+__kX%{Z>&RZmXwCdi!gd5#fJ|%lv+{G zr|b#Ts1}Bc(CPkXaIO8<1+}HlegS6DFs7U6?N~4wR!^#(;YIbqQIOqp)Y>Db6o%1i zfzY22V-EN1GJALyq?KWSwMGbU#gV_$)SLlMlxrQPHdgnC(nU9*nIG%)UtAL8sRnL zvIO*k?9`K4fpnym;50z#ebD=+rZ~#B9dpG&=ZI-%{LqY5j8ndz5Bo^s;38&v8 z8(1+}&NV9Y(=RCMwyd1YBBL1Mc{4wI?k1TngzL8oyymA8O_M2Y5c0rtPR>#ek(4}+ zvTI`PjpdGC&F~Syy8RdkeK9)AX8N#B63UrIl;U;paq7n-;aB#n!Um^KDkm6tH=B)> z;3zLTI4#Y?2aYLOw=U)%ARIOAdmMMfhQHaQE8 zl3Cp0zQYq?6o&{k_DNXPel;f2^58wLpT=YKQSuc(*4?S`z@Dr7Qgz$FS> zi@ndTb$lk)7Z!9l#jnB&dk);SrBnVL{_rebeB*2~oq^e;zWdS~RE>Hv&Z771FSI9J z`7tfJM8x*5sOXA1eyweMto(__RVTbyU+|S5HB6d4Dgb*jRGLh3<^SP_w;CaD=Airn z>}rapX06!=({QJ<^CD>ewmorplO*#Ve>)f5@p2FXtSj8Mpa#1cVXgVCAhb)&HQZgO zfVQu&2q4IMN4mO)pTC13+M#|H5NTM8&`jguD_nAjiR*oJ9i%> zS4&QN%lZcXJT1e1N=#qGK$_eAeJ=b0Pj(!BY81~$?SW<-R5^LHJW`}xjV$cQ>zZPC zKx&lIPgkaTQ)c#4Kyjmtk6@>u&~kwQ2TO1ikDO|0e%26uY|$`ZJ&_<<=Iv{O|s*<_~}Z@laTeJVr;$B<`4hA&>B z`VsH7-~=}Ol<9at3?1V^wg6RL>j^EV032~4IaYKQnNnGs;Ssey~SyhcqT&3YZz z^xJp%0v#<&D{~;^r@WJWG&QnVUIZ8B_1fEU$761g0RP4%O(ohIte>|q%@y#fVUTSp z3>LLub23p7)|oran=&|5TltRGRS5ieG(9k&xel^Z*_B-TPiOvby+_(mUYMo9snsY?Ezus;g8M8RHQ1HQKb!kSg93n1fGkNdIc0U!-ysgq$IH3AbRuiz?4Bij zYWh9M<02o0X@!^fPTv3#RsP8U+2+zhe+uFtd;k}gJ{B&)4M?v7*+E_8dAcPbqo_^x zN&n?q>huypF8^2I>P9V?K-3j3cj~Sg3)t*kHmSFYY^Rj0R^WO+zrdA>zb*);SAsKF zzO1Jom~o%=Ys9O930x;UXCGHc@^7Y-ti47gI|()f)IYW z$3fiwh4I*B80cG~U)9X1S;3M^9XBn)VR!|^m!=!!5StHKz1RF)YLD6rKN_34G|QL0 zKgd6Bn6djN$h3Y{Ry2=JT*nJrklI3~GExg!unzW zKobvk_}QhwMzP#-rWz$TVa+W>$uZzVkVFGW1J%yZ0pL961Ci7a9i9N!$n_#r3FezE zOHZ)9o$@3746}*BvD0BoxzP%LJr&y;LV(?#7TH?rU+$3b@WTW60#_?*alt;Tj~z%X zQF(&yC_MUY`Jp#1DJnKFXT!AI5*5$5uc-3GE^)elv9tt&zAc`sIBZVPOodOd+Z*@? zWK(gmvtB75yypEXBLYk`AId00OCj~^1m}D$m@-oSre-{&gxYjaWV+lV4QFU_5@0@j zL6R!$xqlPc&SZURe|EQNpsee&g^;WLTLuD_$RMf}-Td^i%EEfQ1WR<<(6B`%X0%ul z2`V@-^T7|#v|j+;g+5$0u0cmpTQP(T{|vS69iYie+5@#L9^B-_u+ngReT=rR1OmTL zQl6CA=9<629#ARBwi?mA;yXY#kz$+8cUQK`kG*lpP;nG|&N5M6_b)@oA1%Qv7WjPI z(SmcSv8M5!NJZY3RzQr(%zQ%MSHbTc39uFT%-D5$%?=#%HU3Q6g-;4D!R_B*qE#P$ zOXwG@E2Gnc#f_HO06T_@ab6ARqIKGm&AdvT z3b1cEJCIs&T1NEg+Vvj;j6SKtPl&WCxUEL-JF0o+tDCJt++z9Q7%)PB(W5CBK^U|N zRqFH2`*n2X%fIK0V)+?+1L*OXbc59gCH6_eqEW@lBly&2dpvos9YznAH8#^U6@ecj zZafSH-QrDi-&guLMk4iH^}N&i@R3THFYO&m=+(8l!P?3O( z$7nS)&n5?siowwtBgNueMk&~^5GWa^E4}g3$+BR@{HTzgf4TL0;guS1N3q+ar7FWg z3w2gljup*1G0`4xK{n&yaD6xzy090+eA#I4cE{r{-0U$eeiUScQuH#ch1<{XFdl4R zpx2p_M!n_(s?;bBrPz(8w6LSB;n~H@Pq3E9Y0Y>}w<*=Kvv)q+o33O#RX$$;6MU@J%jgsn+3Wf)+-J@e}gPv?Yl%+nih_ZDJ&GFhYI`V zBfZ(KtL_L zSa-p-CPLUDxbB75K&bobQ*(lvj#0mb2z?5#247Q)obHkRLp2kpS0&9p(yMOap%ZaE zQk?9m-l;O_6-rt)-{&zUNJw3@*V;G6gGj3ynuWC0_uj9DyUYD2Z8w>P91szRH!K`T zNIQhRBIun-s-wd zht_q;s;7o#I1yba`Z+|)P?~N5wBXPgr->&+uafcZwDNcUR3TYV*7MX4T!%ebJu&2a zW_$_rN<{itDR*2LY_NZ1)>u)1@~*)9n77rjc}>b)CM zGkLM}d$a^bV9cYD@m(Hr^4K?e%V&%Ae&I)O6P)CnzM1FJJe);nhhGD!j}srT){J*R z9}Y5|zj#4<8Xq6bJ|Do$Zm@e4=LT!=vrRUCoZ(!q?0#J1w!~$7*_S&=Ow;q29_h!86t*aS)z{wq?JrYAmqEIT(g0mwZS8M zX0uLjWbyN=*52U9QuB`tcKls!9PYJ08NbB&#H(JK=Jj<6=8XJM`tywQS7{f|&gQl7L0A(^LH=&ZSHuG5j z)ZCE(4MRDUVp}qmH;TsDkZ$$!&7~RELTD9P-Vit?GxI%-S)(3;shT$=$fSIn)>)!4 zRQb|6f{|e1ENJ8Y@^d$HF1lkoz4R-(Hpp$RqgpP1rTJK;xJ&!EiqksWrATQ;<3VWK z@`uOV*Cc*=9#Y(QBqKif;?F+ktQf&#X%H{6D~LZ$YIJZ|2)_`_{B_w zlW=%8r3Rk7q`r-WJg!2*bHW-21*m;k*{WSs9JGOV!F}Niq^*p>`d-T~-8cFX(5huU zDt!TFB_yA3qmTSt_tMw5{$X-d8nB_ik{0fy| z&jmqt(}En(b$6z!PMk^d%Gryo!u&iK4L3*i3@tl6TT8u3z1ej>dn`fCek^gXkZg)@ z-Mwn$?h*x9@yM5uP|0b!Z*M+RpORodf8g4=I(s)KI^*)6=bW)?9J7){1WK>*R_h8N z1-ILWzEzwFJ@;WD=MI1J^Bh7{VXtS<^?L~+7@4_p)lTxvqF<@*bi)C-EmH&+FMH{bU>nG@d&KSe}Jx6fi zz3>0Ql%3Z64CWeE=M@^D@!u%D9y$x{KPVg`fD(ag#HE;59$}SH((CIf{$S z90>(#8tnaQK$(McyPi6FelH)_)EKuI{y(;Mq8O6+i8}}1D}P&9(%7Ufb4(-N#Z!aj zJGT=wkNYX5B|faCP!XliZ;O7|*7z0LTPGWLs#qRX?L>W*op=jZ68-f1A8A|9DX2?z zuHrJL;ZHwz_j)adWTO{LbQh=VAke(EQ}PeOdGDkmC7AWE{t|&k&p#Y1?Ycnl960v;WRPxkOXVp{lSKXcb#XI#GK2n zC(N7fF^ErWLq8mIV&QEudgMB2=90(bXvMmblq*5xH_PGJ$xK{RGVWK`B2sT1? zCVOeBO;7p$n?Ku6UN<2m?zfEQMNFkci*&7GF%WR!2W#$tPWA?kXwoU&aeI0I;5$Xf zSy$X2Lm}cP95R3OJ-;sC;d)Ii2*Gc;+bP<7IASI^f(Y1%W1D8@7wf$E?SR#G`3d-? zD&k6TaXSN}kM@687!l{_X=h?c|92b-YG;rHxAbzD@0enk6Eq}*r)ACLuc^(rJjP^r z_>~Y<+&>fPe`X-9va9Ckj)v$r-jfZ0cWKBufJfz>NmJ>g`Hnddrp7bu=P@#T&E`^j zsX3(Y5O+qC{AGMPs^=x7P62Dz?78^_umH(weN&5}f$&*3Fyi^!Cnt=Se3WzbboBq% z0w{|OosY;Kb4tVwNhN3@YZb>A%9_ZB!|&x*_T+&M=V^pv+p2CwrDXnIC;(qaGrsXY zfjy-P>wh411asTXAXCi0XSb}OIw)gj0yo2dBlLb}VW7e6i7%x9fd@QpXM-$6 zPGEC+&%v^XbYJ~b6hYkAi36r6M1OSfiR1Q{+^V12<+=wF^1&AB!J?wmt15|>Y(MrZ z&iB&x^O@?_hL1+vaE93%EM&UbBh7v{6pe!a3%|+Mlj&Y zYu?o%IoH4%Z&>q1F;QR0z^;<1rMlWBMp@R-d!H`kEtJf2)m>w(FM0{5yfNJ4mBf7# z*4Xb1Z6dHYU>XiXiL*n_OIdv5b;0<8>56biwqN(&7TJUgzq%X%0S3Rk??XgA10~x? zEYq_O#}K)ksqzX?c%7!YX~}u|%dPh!>H0l-cu}G0lRMyXKLaA}^ndcCn~jk9|DQ<3 zCd#Y?M;mcF+cOfK?1nTZRUH1=HK9Xc-B|lXgy`5oDM&grq7;}^$3U-gZM%{NpTFv_ zWw?xc8Z<;gem`#kOcPb+dVaMS(l`H^vTkbrs`riq=cr-cRa#(mrEOWMhP5~ylhC4N zQO}B|Y%w+5JrwOGWzn`E3TO2Ex}rKoVO18JyMf%5P44**;$cfSkB(O5^TTR{Q6YBZ zpE3ABQH)m(WDGrS8>hc}TtteQd#Mh|);282wUJ($#x4vxVX{(2xxE{boWXI31-(!JZBo_}fsThDyPlTS^^nGXF^tpP;FM~%w#G0ETr5Nh9sTIXVb{P5V0?cZsSQX6N z24!`pnOi^iR}yJwgO&7hyeeLr5(R)~)TEotk$#Q)v^0eBnEwe&G$6H36yOa8Uu5v! zxY(@9Mx~)Vy^efWnh@`E*N%?bm6yT=Gtb4ZgD%DkF7c!J-%?Qi`^JH`{K=@-7H@CpBQ`shI}ngXIP*}-3sRp^ zx|jW9%*);;7 za2c)&5Tq||1nXbOt^H!hi(4|vca)5?EU%QHo-4RH2@TlIe>moVDV9M@}G zgE#^qedD(@@I)h{$g0ru+pjzC3;`1nue1jz%|xp;v|E0m-+;p8{+nI64(jGO`XKQP zf9OnPd)Np5daB=rgGt9}!#6e%u4av;4Dd^FR3X~?R~Az^(sea-A-QPkmV|Ms>3Mt4 z=@7j~8|olEObh3@9P~FQX*Ix1axh^UAq+CYFIv&R4V0QE1=;x0!;vF=>0Y zi*d+|RAB})jTK$z6q>Btc!B1BIE$AuDk{G*d?&!#zx&LQQ}?wk#FejSPT(|J#I!;z zPlsdlTW|silt}{DE9D45a|HR0C}Y#(zp7r!P8T#8D-E|U>L;fZE=Ye9AqOa27Yw6) z4o2q+fd}X#)qxzrpRtqUcO?yHywgtLbGL!tJX#>@zGY!L+|hmed_~saTmMNrFitc5kEbUJ)b6i>a`#B<6vA@{3m6PV%sDy?)pz!AeEc_26LWhe9oh7SYcq3 zQZlx`R&|`0`CbTXjN-ZDddOg7t2E>RA)5(kc*@{iI#p&Cy|c2WvDIpT9;>feuV=CB zwTAWVJHJby!m0jNx54F5!;Xr`9KW^0>Z82qGUXRV0d}B;v0$@D%IzB|Wh$C2_=cY5 z*%u&~(4axYR;;(i7>GKRI~cU3i%;IGUhYuUTh+6K`>i(%uMHlZ_urHZgU6w{0Fk*O%9f>eXpe&GnJ+BO+ru=^X#7>_i%{{La5oqkBzq$ zherm(wRFxkcj$r)3(Uc$dJ+cT0D+-D?_2b=V$jw#i-v$|r>wXK&h4$d?{cD9b-YmL zh_S-}IQ$uEdho^52Br)!gyq@JWHZ-g{MF@3BZ`B>+&l)K{NS$nCfC=*AM=|vi@+KG zgBF9Ynm?i zjJv@it|;8(o}#i8&yu$(B`ZL4q1aO~l(_OmV>oy1IDe3ji`F7usIc>n}bCsw!jv46f?k zaPzw#e*DUQT?4HxV8lGF{Tzn^{kLFFjgp{vb+RF*VK+s)1*aE@aii}`IB&<$g7cgW z9XbBL>fmqs<@DFejOb}$!9`y+9O{hIg3CTJybR?h63m?9re|Fwn8jn~s7yUPSG6zd zk~=htz6)9sq#eenYWfiCabC0h(U%#@6UiyxB<5Hz7v;ggfaR2g!n|s`xN&lYPZ$M& zO54nh$_8=(JOJBejq&70imP_=Z%5%ws%?Uy-jS3Pdy*kH3_#HvvRRt8x?JL0LVzr% z!t1XkK7j2j0o@juepOD%8Y)RQj-Ffw)XP1Q&}4RgLS$QZD^NaoKz0Pi@ZTb}ikB;a z%&$iaN7J1=YrIn!TK~4GByMG-JC+OoHpio$;>LtgK;-*eq+-elBE52-aS|It7_^#7~pwm7ESR+U~T; z$2TlS2HAZK^Z?@O%E_I%qT<_%Bsa$h7?=#7oO7;~M6w7}M$Q?q-u0K_2mec8Odcno zk)zoCD^i4gI?$PDo2*1WsMV#TiE%6UInt^~nV$80<1%w}+b^H|S9U#e>fzvMl{Kub zsThEyupI%QGH*HNsM<*?nzGyE)En>lElv*GGxDHb-_lfNvWzMWp6PNP`r<0I!osxO zt%lG(2cX6PcQ|@}vbO(}Uq+OxixX+nr|=J|8908(2cF?L3gOyf_VDeW3Rec4Re+!}TXdq&-Y@@YSwst71cz#Le_GPldZSw&mGv_KbFe8Pm z4>7iWyJ#i`T?+DMP9JT|laP!IT-iWjyAXh!7rYArZ$nZ~iXQor5Xil%{+vWAGK(h3 z)b%RO-hL$LIs4(HBonFC>mE43MGJKaK>ko@+YqdrPtBMIM15E!*^Bc<_nLx0uUc`wo6+|5@e&@E2dR5#|q8uTwTv(|%6BYDp-(xGCv|AV*N46ZT?| z+GWyq6&k^3sFbJ}+uIK7$M=9R|6gq{P zL9bukyHQ!D{z(g!e8m`(TJ$Vli1~lVyg2!Z- z4IhBuvTZzn11~EYTNEZbZ}=CyqXHH87)yE4K&Pp+C8G{N8C5Fz?a;hZ+)Re$!vdm2 z%K6=S`7@?I?FPp|K?1B9DzTou-Bq*C(6W(LLtD};xz6v7vqN-FhMrryK`Gw4ZW_$b zCIrE%FsXdw*Qxr7kqDFxXa=A7I7OB>YWcy9)Gn7jyqpK6^Egw}@&G8rPIvP#Z7{@` z*ZeL>=KxvXRs<_E_g5Q;(a4N3Yx!zEw7Xm|p}PY6#^CN}Y5kr~TA^u2SY?DZ>b$$#u&f z5-8ngsz?vx1YRFKyHxss&<6c8Bt2PB$}L1r1`kf(;8+;6=N_;y1>~$1yRlU>viMYy zrt%ZCNw%?8_|3(GrQQvzpX0fLWd=KY z^jv-AZ|f2l2$i`cfE+bGt!W(cQa;IKx%O9OM#hasU+G)f7GyiY8nxGbr;Gc;x8AD) z5eRe*Bjc|03Ri8V=27PgtTmlUYh1Jsh&ow9YN>;iDxE3iN9B_aW zl!{Z)-xYibcWT5l*g4x|R9gypCNppdyc;XlCoyZXtFCHq3)=cBVNsNLGeBYv=xE;f zjJ!4mYTR`b37+?39v1?FCg=gLw5t$^!&o;NEV+`TF};LoPXp2_Rf^G9%hZ^KsvLpO z6t#;xsUk6!d~{h+!fvaHl1TW`vj{z4G}Qh4ex-98ERs%8Uf2rZHM?i7yHD%uE^I}S z=Dh2a%Hn}dRP9u0HA~Yedg1)`@*h&i)Z+Vrejl`77{cIk6)^rO!O8SCI^>OO9Xi;d zi<&l>;8T02Za2)?TmqzgL(PSmE?&!S;iEgThq-Ht9~Ck!iM@{8h_kwvsRxt#vTb4+ z@y3QWna3wo7pFI>Vg$_!mCjaVI+n14*FXH%wZDOk-$)E14NXbrZH~!ozvbR4R5ST% zo3w^XFoE#f1}Iin=_;2heFfw1xCJAMUmD_rZi=UzdgzV$Sj}Hr$bXe8z(K2IS&#v6 zW{th3m2A}yoba%rUs6s5`BG`G>wT}BHW4UXf@!T@8YQ}cJcr$6aM6XHw@~z11ft1} z&`q@t-DAai%JUM?IL?~I&jJX0@CXDD?>aSTUO^FUC$l5LO#_kO0ly7bz>?R-EHul# z&rDeRu(@P*_Wb@<)G?(;iqF9Wycqn@9f6A2+c9!JtZmx%edI}?I_9O5#urV;o3%St z1TeFQhV6D-C+;S)W?7U~ij~T&3vz?Ll4_``Rec% zJ&8B%Q>0K^@N$3%WsY6IY%E)ICMI=%XOQ%n=s~SpV!8H>kFnCuNyk$BdAHlKPEuQf zf25bmFpL2pa0OlY#b{D@#NMIP12z^7^DWzU%dl*UgaD-GH_BiFOh&kYnUfXa#-^~K z$W_zPJ3}c}6if6tofomM!h{!*x$Z1naDh7X6I;Zz}y}kS@Zm)!~G)PF* z_;uO`yC@e-yB5l0rfCl!Ym4KC-uAq5N;n949E-*|Yfc7b4^|A6dM-SQ# zO2v=0|D;FGTPsW?Td4=wx_P;}`moZS0kLxp*QG()oQgK?UEQrB!}nj&bBekt z%#Zdo!X+$GuBQl@zi^R~Rc_zvGfooqh5a*z8qbpVV1Mu%mxBj`nBT8x{dK_?Z|+Hg zQ-4v}j7)#+{D+b`?vNkB`m?@!Mx)^9tJNIY3#LETiC3gSyC@%?Td+|qIM1lJXQ4!K z>aYHO-|=zzhJ_E*BTAp69)9$QCP@QFhE$|?-&rQym~W_^-^;=9Zb1e*QX7t1$m zVvn`n97Oj9a_!pUEWp5_UHzXdcvH4vCvs1c?HvX>YKG?`2%13_FE_6J#4)A>)!kx9 zhBY=C%J6LC+9%wVsdQN;qrtyF#^dXrBtSY1dU-10qxLn%SX@$hQnAH`rbmy0UW{KL zFepHSp!z0YW;MEd>O+M_>k9+!X!6hr04Ljb{rmeWS@&I((5HH07mR$jUutx}OjEj( z5jV(qa^Qq3$BLPu3U}CRHUwd+h`kvCOzlJhcoDvlWE;6z&gR^d3ny;$da zLD=TQ5Kk>W(Gzj{l1f=(4ma;*!>g~cQ&T?UdR5mK96B)b#bd+YSkavFDpPgXTN)iv zI$%IiAO0|GXZkSU3{WmP{g=b}HJi9o<5q%9Uw3Q=C)g3XcNm&tz%!CT?MGuy5j+E{ zWk0G8;bjx;N#Cz;^6SJ05!Bs9u75geL!!YIZgpE?=kyPM?hk)yR{L&M@p6 z0=o_0J?pM1{nfkab}xjwy5~~Kcu<&Tv=+K=u9!ACZ{yThf~i_vO@~~4(<69jiT;3Z ztzqQ_dPxb)9Kp!uDR!#`UlF_rkvm5Lt4}_8VflB%p1wiq-nF z+&-22bN1PM>jOah|I2CF8l5VeZd==>J@+1$n}w%((wrVTsfzIwDSm{(t?RfYof(3c z>6CAR+hor^y%9valwt>}JR3LlyCX&C-&zSHu!g2_3aaOj@r2Ca;7m9HyzwWk9zkJGuqm?*-vq5Xby!4a`M$&hr30YX z?F4bxjOmG7)br;)Ul)WOu0>w%){Em8Kb$J{Ki7mOj@HkB5hlCwgUVStwRB(`$msn3 zW68l6_-QmuY@|h*k!h-dE>&&v=30 zIv3(Tl=pJrKH6z|rv)q59=N?as&_Po3H~a==sNM|4X=W#K*8r$N&#WvHVMQ8zDzLd zV)Dt$dm^J%7u}~piF^kD8Yp_Z&Uk|80}tRszg$ALiocA z&U(s2XW__mKc4sym@3MmQf`RaZ2ZcnKKE3-oF85QR&6*9*Yoc#x~^M{;7jY+&Nx1t z9;OP1mj0CKUwb(Wvpa1A;s-a3=aPnOem&7jJ&5aKY2kjAi{EseM4;=;;4Y}e@sWF= zA0G=hridbHd(+pd7ntI!Pli6S)3UB0XF*&6?nyx9LSypblGr5BFXg^bRHDaZeGF zKYA6I?$BJ$!L3>1>)B@=SqdDI3o3txyAWJ%X`+7$fgnGTVp-1)+LLdd#y_o80#604 zYlXS!e-r&*Hpl$YNw?FUCO!B6n`0ac3lmUA*{JK!y4vN-5Z^ntAy0%#PdCo!;3cP# ze=PC+U8O~-JElo5M!ch(!`Q83c7(#bv0mwAFrrrE5)C~5ch4R(H$BOIVbEpddh3J; zWYV{|9gznU$MoW0C(72_{L`{VHwf0)f?kIvSV!PME*{ zhd_id>2bhvo;mP@Wgu3p2Aky|)HjztWISA0VuGkm!N0#4W6x*^BIJJva$+1S*n4!) zCiO7Sgt7Qu7>7JKB)^RP#3H8x*Ka+C5rq*D8&~zJvVh1l@cY*588DzHswso`$^0{< zaeiKC>U(5clg*a4F7Y$QzIfTj!#wdNZk$~Dm((($rpWbbXsHY>Olrl~je|XOJwK=N zJSBwdWUS7&7){b$u-Of~v(u)OBQK6!AROCBQ@p+q)v&k`$%WuAmy`q^%nA*C8_Lt$ zy`sJB_R8ha=<5bQu#C;Iomk~$cR_2=p{VTaMRN^|+#-uw6KJym1SZ1#h}EA(huyCK EKU&lfD*ylh literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/single/images/callouts/1.png b/Greenwich.SR5/single/images/callouts/1.png new file mode 100644 index 0000000000000000000000000000000000000000..7d473430b7bec514f7de12f5769fe7c5859e8c5d GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQC}X^4DKU-G|w_t}fLBA)Suv#nrW z!^h2QnY_`l!BOq-UXEX{m2up>JTQkX)2m zTvF+fTUlI^nXH#utd~++ke^qgmzgTe~DWM4ffP81J literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/single/images/callouts/2.png b/Greenwich.SR5/single/images/callouts/2.png new file mode 100644 index 0000000000000000000000000000000000000000..5d09341b2f6d2ea2d1d5dad5d980f14b4b05dfd2 GIT binary patch literal 353 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQxaY7e*=hH)_rZeB4|imU1$R#1`!P>&$poQl;nzm}mD5ZFopaX|GsS%q*{P~< z;WtmO%lhToBL0i}yfkaOt?EN=nkLNGuU`ywhI5H)L`iUdT1k0gQ7VIjhO(w-Zen_> zZ(@38a<+nro{^q~f~BRtfrY+-p+a&|W^qZSLvCepNoKNMYO!8QX+eHoiC%Jk?!;Y+ zJAlS%fsM;d&r2*R1)67JkeZlkYGj#gX_9E3W@4U_nw*@Ln38B@k(iuhnUeN2eF0kK0(Y1u|9Rc(19XFPiEBhjaDG}zd16s2gM)^$re|(qda7?? zdS-IAf{C7yo`r&?rM`iMzJZ}aa#3b+Nu@(>WpPPnvR-PjUP@^}eqM=Qa(?c_U5Yz^ z#%Y0#%S_KpEGY$=XJL?(l#*ybuErX#^g`ttQfwnX4x42*}TIo_3IbsoNRf>aVMfsJ4-Q{^hZZrE#!3~DHIyIo;*1&0#S#R8GXWt43k48;BRp7)N)S|- z1>C&kGA0Xf^G^6@Z7$n zMFutQvv~;*MUZYF%!pN!TPX!dM|v*>m&a&)K+gzU_K;pxx#tfwf0eF z{6Aql)Y@kWdT@am_mNw@Hu^kjk`}>q?S9@-*pQ9}E$|ZbpD$ zJ7Gs5k(91tmKe$sLWmTGr7Bn~6>1?^s}f2PnR1ciVOW(27K@ZZwFriDU|1uRs#UNC zk|@PmnnA4;FJg6WABDMX_@ZBe_In>oi=V-wDld*vq}M`{&czNeIY^51IYKm z+YndYXy6niGl4=H0i`alZHn}h{(U<^L zrtUaM?H&s8E4km@xW3K}2l{HU9i~Kmth`h+4sGW1O{z!=XlvpWuu5{!5G>RAz< znNpajYLE!4(n`0h>bf?klyFK~l|n4NV{c&BaNx(k-xgpQQV0LH$NLOTvccoMndX$f zkv4mGzNtl?UYK0aBDc10gsL-g8W2sRbk9iJu~UP(7WA#TNlp>SE=W|=i?ba3^wOkX zY1is%HvE3-2vCryds-HJ-mVLw$(AH}m9SyomW73XDgDUw?6|$#yv`%qJ=msel*Vsd z`|NMp%}*;W&Dk-k$XtAVYB3n>$I&|I>ii|Z5HGIbWfAoEvR_xGkdB%u^EKNNweMm8UVjt>++|OBa{aNdr zkhTeJ+;4mFaBq$c85rs58E(yMLLIwHirO}q+Sd!Qw3m#xW&y9rVdPqRh?Qi&xGn8)dVXr!%Zc z@@k>;xsr45PU?g5+RpNiKfik6%9)0JRg>pN=Rf~LS%*%J3sntBdI_ki7mrSgrY^vD z?%WakSLZVrOHS(4IhMeO)hAZ`qU!_Mp^Kl`T85(DsckjoMLA#nV=_NP72jM4aCVNw ztsXF5STjDhYhdzAZ@x-km?7(f@11e;p;vCg#|D~KgRlFCJ{iDQda7PJ;=cu2XOfG+ zz6j|L)Ul6M@PT)tsq8TVCL=<&YucZ z==FL-9C+!x)fov8UwpRWZ~rLo*Uiivij0;`w-$cGJaBl_kilhr-Kmeg`K_}1x&xj} zBcQKVN-2MA=?_2j&!&wDd> zw}p{f$TVAeLb2U>0f{&UE>x@@VD|&aWW35hWduOkAqaC|ZvHiolKf1HK zzu)h>-_Pg!p50|ED_WP3lt81=*6DR>6SZ!PJ@IkW`;%iIE>KG%sj-n}UjrG&0ywSE z>8r;9y%%f5O*rOkZN7-hX|y<(+hQYahEmkw^YXEn4nN}cQ)n7Zo*(gJ4i8QO^?0M3 zP=NP-H46f6rvj{$7$AdRg}dCkwg7H!E3-J-JPw%?%+CYl5tJhE;v@z{yiG(9jVQp! zyePGgi3K3=ScUW`z$Z@G3`RiZ3*dl+FXA~M7zPl84~r!T0&@W&1PcWabt61jj7ktx zm;*e$K+0Oc*?^kV+NZXtlLB;+q#qRs!r?GKEaLkDjRIIElf^iMLLQ~T3$_v@7U2;= z#tMTP4>|&FKk4=nK#UQq_qC7;kn;3N2wuOz@Qj!UK1~#rGC>6M3t&DZ@Ooo$J=PAA zCj7r{JXbqtY4zg*6CU)n1RPX78W<~JDtF&)D5gkxgKi4AsiI&_YM-OUixZ??tpKSn ze5c!qLLw=Z#T+q|BZLqs3`%u1gPQQ^_OJRXsZqwOD&qLO2*a!%fyU`U&AilhSE!u zf#RfW8Nca8?LYcmzi;^J0$aTLuk(_I7B(1E%i{iHi|z|Ja9*KR}4%unPJ zFw4TowlS1#GO3H7Q31*c7>im^52SWUc{QwoqtQYKQqqoI_}z^Db(y?bEU3*;g(Uk< zbhQt9Q;Rl4_Xd*GuUR{_5VHeEE0C#yNL!dhWt>(;lnbF3j@_RUxGA zhlU&%fA8^*!l1Y?gk+ci-WE<{Z}q7&M>qEshlgBmoET)9!8{*KHv&6`TU&?mta6qd z7iwD&9iFFcM~&TiU^y@_(iItM%&Y+Q4fzTJHodO2br<#Qk8o=Fh6?xiG;t(<^tVlGN*YwHYbN*+ux#qerwpu9`;s z-h^IVXo>ux{&d`$r9Z!%mi_6zmY=<_(Aa4VWq+kPR9x~xOWlpzJxnYGn>;_NtFFtp z54GGsQk4p=t-Lq$;+whBb8|*17xjJKQ38{*G>h8VSmBGr5-Z@b}+_3*Xjg7`HBiDzyy{&6?adFeNk#BLg0d5b-3 z9p!F+xWNDCwRfkhhF=kO!^16Ky!0x2slrhor)q_mdPk(;+PiMET zz5h+ansg!r=$v-@J7+7{oa2j2pl#+KRU%es&<_a|W z!QKDvpGsto{Bi1?F{rbP{YmvHRmJgSd->g=lhdE>DT$9i&DZ~hSKGgD<3Nr~x0crR x@l@~8v%fudb7|Fs)}6WGzYSl#_Wjpr@eu7sVJhKCFm=a%+M#HR literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/single/images/logo.png b/Greenwich.SR5/single/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ade2ce6ed9d9e9f2f4d9c5729a252ee618a0a5a7 GIT binary patch literal 4387 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 FV1jwin)(0$ literal 0 HcmV?d00001 diff --git a/Greenwich.SR5/single/images/warning.png b/Greenwich.SR5/single/images/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..0d5b5244605adbb7ab05a1549746a9c35490f95b GIT binary patch literal 2130 zcmbVNYg7|w8V(4q($)50y>JmGlLW#g$xLn}Vd2Si)}#|ptPAQp3Bp-3!-lL0;i^LY?;i#f0m5s49g z3h?3rDQg~EDPlm?FKkgKIcWEK-3X6YU0uzs7H|nq84s39r2!5;pF?SI$QqXy^Ko1x zV~zpENvp@<_Bsd`5MabC#3rvCq&$5dg43yGR zAdy0-rWjC#a1N_+kzUMY#pmogD7!DP(9dEKr3c5ngvUq_6>}Y+w-a81v=eSXnIi_+ zI?U>D1q2C!0zHox#XXKH+@|&rPT*OF5yvY$5J|)WwLqnU)c-5;=UChSlQkaY3@^|g z|J5#YBB}=i+n3Ex9bS$P?xJSKLk)-HHII@;3qG#TG^!+aj>0QkO~7kB0+|yM;YrGB zF>Ge@isQ#73%Tp#k_(tInh3U$uJWY-+DND*o}L-CB5g@#9YWWw~pxZ!Obm>yN#ZHFvL0zA3vNCTJ=t?)~`2O1M{L6un=ml<2x zQEYfifmA?Pz0tqRs^2Utt1pU0BQB1eIY0U_I}c1Y#PP7Ci5s7#IJn}xK{G+{_v^Z+1V#$Erodv>d}ew>RP0wzw+GWkGCBlS1Oj5Z!5NK zUuSR6>pa}RSEZ;qE_w1l#`hQX4E67U6NeP zHZ`T&wj0_H)t|Y1thUqnhub?%e)Y07;a^Tq%FO&iQkR&`qG!dh*2WfW(HuRQj*_DI zeCD1bUA|tMwjOb8E|FRIn$pzEU!2j_N}1Z2ig)vbr5xA0rjC70cmI6*%Uf->hWzaZ z{33HABO5q)vRhzDly2l691@+q3O{}NbSzx+I*k@|L4&3leK#$>u+T#TvTELfKb|IH zc23atf3vmfCa0&TWY@iuoA_Pm<0~ttEC*ut`isb^jXS>X^Sp=l?RXN_OJ*&usGXg$ zTlTv;Ch^vhLDf&+62jl64*&D+=+`sN_dap_1W%p|KQVYIdgPL5uRE9(c4On)DXndL zLNq?%!-u*zmMxyYc4m4Nr>>=A=s}PkJkqr&E^b9>5g+}c1%X}3|WKcg&spcJQ)05zI<<5LTBhNKRg$XRTi2{j)yl_ zj4~kKOvr+m(;vw~9zVh(zC-y9jqQnguz5r7FRq^MyKuXAENp(TAzUVtg&Ts+B_s7) zGn+fN0sG>9GjsPLKTRrvCd`71IZulJ1_1jO9KWbD&_@2UC+LTNpxxrdz#E|j|2nA9 zzB!UTyfAEJ&#%$pQ>QX#8vk@_a5iqukMF;8`wRKe{BI}5$H%OROs4J1#j)|?p|YSh z_SpR^e`VE#F52;WL{!+L(yZLRh40*KS;@box;9(-tE)`mcVp27O*>Z{_Lb*5T3cJA yr~0nPHtg2+UHi&&$8ha;`+hiaUmw&!n@8(?5PqF(KE5>Ym)EG)p&uyBP5%a8^# + + Spring Cloud

    Spring Cloud


    Table of Contents

    1. Features
    I. Cloud Native Applications
    2. Spring Cloud Context: Application Context Services
    2.1. The Bootstrap Application Context
    2.2. Application Context Hierarchies
    2.3. Changing the Location of Bootstrap Properties
    2.4. Overriding the Values of Remote Properties
    2.5. Customizing the Bootstrap Configuration
    2.6. Customizing the Bootstrap Property Sources
    2.7. Logging Configuration
    2.8. Environment Changes
    2.9. Refresh Scope
    2.10. Encryption and Decryption
    2.11. Endpoints
    3. Spring Cloud Commons: Common Abstractions
    3.1. @EnableDiscoveryClient
    3.1.1. Health Indicator
    3.1.2. Ordering DiscoveryClient instances
    3.2. ServiceRegistry
    3.2.1. ServiceRegistry Auto-Registration
    ServiceRegistry Auto-Registration Events
    3.2.2. Service Registry Actuator Endpoint
    3.3. Spring RestTemplate as a Load Balancer Client
    3.4. Spring WebClient as a Load Balancer Client
    3.4.1. Retrying Failed Requests
    3.5. Multiple RestTemplate objects
    3.6. Multiple WebClient Objects
    3.7. Spring WebFlux WebClient as a Load Balancer Client
    3.7.1. Spring WebFlux WebClient with Reactive Load Balancer
    3.7.2. Spring WebFlux WebClient with non-reactive Load Balancer Client
    3.7.3. Passing your own Load-Balancer Client configuration
    3.8. Ignore Network Interfaces
    3.9. HTTP Client Factories
    3.10. Enabled Features
    3.10.1. Feature types
    3.10.2. Declaring features
    3.11. Spring Cloud Compatibility Verification
    II. Spring Cloud Config
    4. Quick Start
    4.1. Client Side Usage
    5. Spring Cloud Config Server
    5.1. Environment Repository
    5.1.1. Git Backend
    Skipping SSL Certificate Validation
    Setting HTTP Connection Timeout
    Placeholders in Git URI
    Pattern Matching and Multiple Repositories
    Authentication
    Authentication with AWS CodeCommit
    Git SSH configuration using properties
    Placeholders in Git Search Paths
    Force pull in Git Repositories
    Deleting untracked branches in Git Repositories
    Git Refresh Rate
    5.1.2. Version Control Backend Filesystem Use
    5.1.3. File System Backend
    5.1.4. Vault Backend
    Multiple Properties Sources
    5.1.5. Accessing Backends Through a Proxy
    5.1.6. Sharing Configuration With All Applications
    File Based Repositories
    Vault Server
    CredHub Server
    5.1.7. JDBC Backend
    5.1.8. CredHub Backend
    OAuth 2.0
    5.1.9. Composite Environment Repositories
    Custom Composite Environment Repositories
    5.1.10. Property Overrides
    5.2. Health Indicator
    5.3. Security
    5.4. Encryption and Decryption
    5.5. Key Management
    5.6. Creating a Key Store for Testing
    5.7. Using Multiple Keys and Key Rotation
    5.8. Serving Encrypted Properties
    6. Serving Alternative Formats
    7. Serving Plain Text
    8. Embedding the Config Server
    9. Push Notifications and Spring Cloud Bus
    10. Spring Cloud Config Client
    10.1. Config First Bootstrap
    10.2. Discovery First Bootstrap
    10.3. Config Client Fail Fast
    10.4. Config Client Retry
    10.5. Locating Remote Configuration Resources
    10.6. Specifying Multiple Urls for the Config Server
    10.7. Configuring Timeouts
    10.8. Security
    10.8.1. Health Indicator
    10.8.2. Providing A Custom RestTemplate
    10.8.3. Vault
    10.9. Nested Keys In Vault
    III. Spring Cloud Netflix
    11. Service Discovery: Eureka Clients
    11.1. How to Include Eureka Client
    11.2. Registering with Eureka
    11.3. Authenticating with the Eureka Server
    11.4. Status Page and Health Indicator
    11.5. Registering a Secure Application
    11.6. Eureka’s Health Checks
    11.7. Eureka Metadata for Instances and Clients
    11.7.1. Using Eureka on Cloud Foundry
    11.7.2. Using Eureka on AWS
    11.7.3. Changing the Eureka Instance ID
    11.8. Using the EurekaClient
    11.8.1. EurekaClient without Jersey
    11.9. Alternatives to the Native Netflix EurekaClient
    11.10. Why Is It so Slow to Register a Service?
    11.11. Zones
    11.12. Refreshing Eureka Clients
    12. Service Discovery: Eureka Server
    12.1. How to Include Eureka Server
    12.2. How to Run a Eureka Server
    12.3. High Availability, Zones and Regions
    12.4. Standalone Mode
    12.5. Peer Awareness
    12.6. When to Prefer IP Address
    12.7. Securing The Eureka Server
    12.8. JDK 11 Support
    13. Circuit Breaker: Hystrix Clients
    13.1. How to Include Hystrix
    13.2. Propagating the Security Context or Using Spring Scopes
    13.3. Health Indicator
    13.4. Hystrix Metrics Stream
    14. Circuit Breaker: Hystrix Dashboard
    15. Hystrix Timeouts And Ribbon Clients
    15.1. How to Include the Hystrix Dashboard
    15.2. Turbine
    15.2.1. Clusters Endpoint
    15.3. Turbine Stream
    16. Client Side Load Balancer: Ribbon
    16.1. How to Include Ribbon
    16.2. Customizing the Ribbon Client
    16.3. Customizing the Default for All Ribbon Clients
    16.4. Customizing the Ribbon Client by Setting Properties
    16.5. Using Ribbon with Eureka
    16.6. Example: How to Use Ribbon Without Eureka
    16.7. Example: Disable Eureka Use in Ribbon
    16.8. Using the Ribbon API Directly
    16.9. Caching of Ribbon Configuration
    16.10. How to Configure Hystrix Thread Pools
    16.11. How to Provide a Key to Ribbon’s IRule
    17. External Configuration: Archaius
    18. Router and Filter: Zuul
    18.1. How to Include Zuul
    18.2. Embedded Zuul Reverse Proxy
    18.3. Zuul Http Client
    18.4. Cookies and Sensitive Headers
    18.5. Ignored Headers
    18.6. Management Endpoints
    18.6.1. Routes Endpoint
    18.6.2. Filters Endpoint
    18.7. Strangulation Patterns and Local Forwards
    18.8. Uploading Files through Zuul
    18.9. Query String Encoding
    18.10. Request URI Encoding
    18.11. Plain Embedded Zuul
    18.12. Disable Zuul Filters
    18.13. Providing Hystrix Fallbacks For Routes
    18.14. Zuul Timeouts
    18.15. Rewriting the Location header
    18.16. Enabling Cross Origin Requests
    18.17. Metrics
    18.18. Zuul Developer Guide
    18.18.1. The Zuul Servlet
    18.18.2. Zuul RequestContext
    18.18.3. @EnableZuulProxy vs. @EnableZuulServer
    18.18.4. @EnableZuulServer Filters
    18.18.5. @EnableZuulProxy Filters
    18.18.6. Custom Zuul Filter Examples
    How to Write a Pre Filter
    How to Write a Route Filter
    How to Write a Post Filter
    18.18.7. How Zuul Errors Work
    18.18.8. Zuul Eager Application Context Loading
    19. Polyglot support with Sidecar
    20. Retrying Failed Requests
    20.1. BackOff Policies
    20.2. Configuration
    20.2.1. Zuul
    21. HTTP Clients
    22. Modules In Maintenance Mode
    IV. Spring Cloud OpenFeign
    23. Declarative REST Client: Feign
    23.1. How to Include Feign
    23.2. Overriding Feign Defaults
    23.3. Creating Feign Clients Manually
    23.4. Feign Hystrix Support
    23.5. Feign Hystrix Fallbacks
    23.6. Feign and @Primary
    23.7. Feign Inheritance Support
    23.8. Feign request/response compression
    23.9. Feign logging
    23.10. Feign @QueryMap support
    23.11. Troubleshooting
    23.11.1. Early Initialization Errors
    V. Spring Cloud Stream
    24. A Brief History of Spring’s Data Integration Journey
    25. Quick Start
    25.1. Creating a Sample Application by Using Spring Initializr
    25.2. Importing the Project into Your IDE
    25.3. Adding a Message Handler, Building, and Running
    26. What’s New in 2.0?
    26.1. New Features and Components
    26.2. Notable Enhancements
    26.2.1. Both Actuator and Web Dependencies Are Now Optional
    26.2.2. Content-type Negotiation Improvements
    26.3. Notable Deprecations
    26.3.1. Java Serialization (Java Native and Kryo)
    26.3.2. Deprecated Classes and Methods
    27. Introducing Spring Cloud Stream
    28. Main Concepts
    28.1. Application Model
    28.1.1. Fat JAR
    28.2. The Binder Abstraction
    28.3. Persistent Publish-Subscribe Support
    28.4. Consumer Groups
    28.5. Consumer Types
    28.5.1. Durability
    28.6. Partitioning Support
    29. Programming Model
    29.1. Destination Binders
    29.2. Destination Bindings
    29.3. Producing and Consuming Messages
    29.3.1. Spring Integration Support
    29.3.2. Using @StreamListener Annotation
    29.3.3. Using @StreamListener for Content-based routing
    29.3.4. Spring Cloud Function support
    Functional Composition
    29.3.5. Using Polled Consumers
    Overview
    Handling Errors
    29.4. Error Handling
    29.4.1. Application Error Handling
    29.4.2. System Error Handling
    Drop Failed Messages
    DLQ - Dead Letter Queue
    Re-queue Failed Messages
    29.4.3. Retry Template
    29.5. Reactive Programming Support
    29.5.1. Reactor-based Handlers
    29.5.2. Reactive Sources
    30. Binders
    30.1. Producers and Consumers
    30.2. Binder SPI
    30.3. Binder Detection
    30.3.1. Classpath Detection
    30.4. Multiple Binders on the Classpath
    30.5. Connecting to Multiple Systems
    30.6. Binding visualization and control
    30.7. Binder Configuration Properties
    31. Configuration Options
    31.1. Binding Service Properties
    31.2. Binding Properties
    31.2.1. Common Binding Properties
    31.2.2. Consumer Properties
    31.2.3. Producer Properties
    31.3. Using Dynamically Bound Destinations
    32. Content Type Negotiation
    32.1. Mechanics
    32.1.1. Content Type versus Argument Type
    32.1.2. Message Converters
    32.2. Provided MessageConverters
    32.3. User-defined Message Converters
    33. Schema Evolution Support
    33.1. Schema Registry Client
    33.1.1. Schema Registry Client Properties
    33.2. Avro Schema Registry Client Message Converters
    33.2.1. Avro Schema Registry Message Converter Properties
    33.3. Apache Avro Message Converters
    33.4. Converters with Schema Support
    33.5. Schema Registry Server
    33.5.1. Schema Registry Server API
    Registering a New Schema
    Retrieving an Existing Schema by Subject, Format, and Version
    Retrieving an Existing Schema by Subject and Format
    Retrieving an Existing Schema by ID
    Deleting a Schema by Subject, Format, and Version
    Deleting a Schema by ID
    Deleting a Schema by Subject
    33.5.2. Using Confluent’s Schema Registry
    33.6. Schema Registration and Resolution
    33.6.1. Schema Registration Process (Serialization)
    33.6.2. Schema Resolution Process (Deserialization)
    34. Inter-Application Communication
    34.1. Connecting Multiple Application Instances
    34.2. Instance Index and Instance Count
    34.3. Partitioning
    34.3.1. Configuring Output Bindings for Partitioning
    34.3.2. Configuring Input Bindings for Partitioning
    35. Testing
    35.1. Disabling the Test Binder Autoconfiguration
    36. Health Indicator
    37. Metrics Emitter
    38. Samples
    38.1. Deploying Stream Applications on CloudFoundry
    VI. Binder Implementations
    39. Apache Kafka Binder
    39.1. Usage
    39.2. Apache Kafka Binder Overview
    39.3. Configuration Options
    39.3.1. Kafka Binder Properties
    39.3.2. Kafka Consumer Properties
    39.3.3. Kafka Producer Properties
    39.3.4. Usage examples
    Example: Setting autoCommitOffset to false and Relying on Manual Acking
    Example: Security Configuration
    Example: Pausing and Resuming the Consumer
    39.4. Error Channels
    39.5. Kafka Metrics
    39.6. Dead-Letter Topic Processing
    39.7. Partitioning with the Kafka Binder
    40. Apache Kafka Streams Binder
    40.1. Usage
    40.2. Kafka Streams Binder Overview
    40.2.1. Streams DSL
    40.3. Configuration Options
    40.3.1. Kafka Streams Properties
    40.3.2. TimeWindow properties:
    40.4. Multiple Input Bindings
    40.4.1. Multiple Input Bindings as a Sink
    40.4.2. Multiple Input Bindings as a Processor
    40.5. Multiple Output Bindings (aka Branching)
    40.6. Message Conversion
    40.6.1. Outbound serialization
    40.6.2. Inbound Deserialization
    40.7. Error Handling
    40.7.1. Handling Deserialization Exceptions
    40.7.2. Handling Non-Deserialization Exceptions
    40.8. State Store
    40.9. Interactive Queries
    40.10. Accessing the underlying KafkaStreams object
    40.11. State Cleanup
    41. RabbitMQ Binder
    41.1. Usage
    41.2. RabbitMQ Binder Overview
    41.3. Configuration Options
    41.3.1. RabbitMQ Binder Properties
    41.3.2. RabbitMQ Consumer Properties
    41.3.3. Advanced Listener Container Configuration
    41.3.4. Rabbit Producer Properties
    41.4. Retry With the RabbitMQ Binder
    41.4.1. Putting it All Together
    41.5. Error Channels
    41.6. Dead-Letter Queue Processing
    41.6.1. Non-Partitioned Destinations
    41.6.2. Partitioned Destinations
    republishToDlq=false
    republishToDlq=true
    41.7. Partitioning with the RabbitMQ Binder
    VII. Spring Cloud Bus
    42. Quick Start
    43. Bus Endpoints
    43.1. Bus Refresh Endpoint
    43.2. Bus Env Endpoint
    44. Addressing an Instance
    45. Addressing All Instances of a Service
    46. Service ID Must Be Unique
    47. Customizing the Message Broker
    48. Tracing Bus Events
    49. Broadcasting Your Own Events
    49.1. Registering events in custom packages
    VIII. Spring Cloud Sleuth
    50. Introduction
    50.1. Terminology
    50.2. Purpose
    50.2.1. Distributed Tracing with Zipkin
    50.2.2. Visualizing errors
    50.2.3. Distributed Tracing with Brave
    50.2.4. Live examples
    50.2.5. Log correlation
    JSON Logback with Logstash
    50.2.6. Propagating Span Context
    Baggage versus Span Tags
    50.3. Adding Sleuth to the Project
    50.3.1. Only Sleuth (log correlation)
    50.3.2. Sleuth with Zipkin via HTTP
    50.3.3. Sleuth with Zipkin over RabbitMQ or Kafka
    50.4. Overriding the auto-configuration of Zipkin
    51. Additional Resources
    52. Features
    52.1. Introduction to Brave
    52.1.1. Tracing
    52.1.2. Local Tracing
    52.1.3. Customizing Spans
    52.1.4. Implicitly Looking up the Current Span
    52.1.5. RPC tracing
    One-Way tracing
    53. Sampling
    53.1. Declarative sampling
    53.2. Custom sampling
    53.3. Sampling in Spring Cloud Sleuth
    54. Propagation
    54.1. Propagating extra fields
    54.1.1. Prefixed fields
    54.1.2. Extracting a Propagated Context
    54.1.3. Sharing span IDs between Client and Server
    54.1.4. Implementing Propagation
    55. Current Tracing Component
    56. Current Span
    56.1. Setting a span in scope manually
    57. Instrumentation
    58. Span lifecycle
    58.1. Creating and finishing spans
    58.2. Continuing Spans
    58.3. Creating a Span with an explicit Parent
    59. Naming spans
    59.1. @SpanName Annotation
    59.2. toString() method
    60. Managing Spans with Annotations
    60.1. Rationale
    60.2. Creating New Spans
    60.3. Continuing Spans
    60.4. Advanced Tag Setting
    60.4.1. Custom extractor
    60.4.2. Resolving Expressions for a Value
    60.4.3. Using the toString() method
    61. Customizations
    61.1. Customizers
    61.2. HTTP
    61.3. TracingFilter
    61.4. RPC
    61.5. Custom service name
    61.6. Customization of Reported Spans
    61.7. Host Locator
    62. Sending Spans to Zipkin
    63. Zipkin Stream Span Consumer
    64. Integrations
    64.1. OpenTracing
    64.2. Runnable and Callable
    64.3. Hystrix
    64.3.1. Custom Concurrency Strategy
    64.3.2. Manual Command setting
    64.4. RxJava
    64.5. HTTP integration
    64.5.1. HTTP Filter
    64.5.2. HandlerInterceptor
    64.5.3. Async Servlet support
    64.5.4. WebFlux support
    64.5.5. Dubbo RPC support
    64.6. HTTP Client Integration
    64.6.1. Synchronous Rest Template
    64.6.2. Asynchronous Rest Template
    Multiple Asynchronous Rest Templates
    64.6.3. WebClient
    64.6.4. Traverson
    64.6.5. Apache HttpClientBuilder and HttpAsyncClientBuilder
    64.6.6. Netty HttpClient
    64.6.7. UserInfoRestTemplateCustomizer
    64.7. Feign
    64.8. gRPC
    64.8.1. Variant 1
    Dependencies
    Server Instrumentation
    Client Instrumentation
    64.8.2. Variant 2
    64.9. Asynchronous Communication
    64.9.1. @Async Annotated methods
    64.9.2. @Scheduled Annotated Methods
    64.9.3. Executor, ExecutorService, and ScheduledExecutorService
    Customization of Executors
    64.10. Messaging
    64.10.1. Spring Integration and Spring Cloud Stream
    64.10.2. Spring RabbitMq
    64.10.3. Spring Kafka
    64.10.4. Spring JMS
    64.11. Zuul
    64.12. Project Reactor
    65. Running examples
    IX. Spring Cloud Consul
    66. Install Consul
    67. Consul Agent
    68. Service Discovery with Consul
    68.1. How to activate
    68.2. Registering with Consul
    68.2.1. Registering Management as a Separate Service
    68.3. HTTP Health Check
    68.3.1. Metadata and Consul tags
    68.3.2. Making the Consul Instance ID Unique
    68.3.3. Applying Headers to Health Check Requests
    68.4. Looking up services
    68.4.1. Using Ribbon
    68.4.2. Using the DiscoveryClient
    68.5. Consul Catalog Watch
    69. Distributed Configuration with Consul
    69.1. How to activate
    69.2. Customizing
    69.3. Config Watch
    69.4. YAML or Properties with Config
    69.5. git2consul with Config
    69.6. Fail Fast
    70. Consul Retry
    71. Spring Cloud Bus with Consul
    71.1. How to activate
    72. Circuit Breaker with Hystrix
    73. Hystrix metrics aggregation with Turbine and Consul
    X. Spring Cloud Zookeeper
    74. Install Zookeeper
    75. Service Discovery with Zookeeper
    75.1. Activating
    75.2. Registering with Zookeeper
    75.3. Using the DiscoveryClient
    76. Using Spring Cloud Zookeeper with Spring Cloud Netflix Components
    76.1. Ribbon with Zookeeper
    77. Spring Cloud Zookeeper and Service Registry
    77.1. Instance Status
    78. Zookeeper Dependencies
    78.1. Using the Zookeeper Dependencies
    78.2. Activating Zookeeper Dependencies
    78.3. Setting up Zookeeper Dependencies
    78.3.1. Aliases
    78.3.2. Path
    78.3.3. Load Balancer Type
    78.3.4. Content-Type Template and Version
    78.3.5. Default Headers
    78.3.6. Required Dependencies
    78.3.7. Stubs
    78.4. Configuring Spring Cloud Zookeeper Dependencies
    79. Spring Cloud Zookeeper Dependency Watcher
    79.1. Activating
    79.2. Registering a Listener
    79.3. Using the Presence Checker
    80. Distributed Configuration with Zookeeper
    80.1. Activating
    80.2. Customizing
    80.3. Access Control Lists (ACLs)
    XI. Spring Cloud Security
    81. Quickstart
    81.1. OAuth2 Single Sign On
    81.2. OAuth2 Protected Resource
    82. More Detail
    82.1. Single Sign On
    82.2. Token Relay
    82.2.1. Client Token Relay in Spring Cloud Gateway
    82.2.2. Client Token Relay
    82.2.3. Client Token Relay in Zuul Proxy
    82.2.4. Resource Server Token Relay
    83. Configuring Authentication Downstream of a Zuul Proxy
    XII. Spring Cloud for Cloud Foundry
    84. Discovery
    85. Single Sign On
    XIII. Spring Cloud Contract
    86. Spring Cloud Contract
    87. Spring Cloud Contract Verifier Introduction
    87.1. History
    87.2. Why a Contract Verifier?
    87.2.1. Testing issues
    87.3. Purposes
    87.4. How It Works
    87.4.1. A Three-second Tour
    On the Producer Side
    On the Consumer Side
    87.4.2. A Three-minute Tour
    On the Producer Side
    On the Consumer Side
    87.4.3. Defining the Contract
    87.4.4. Client Side
    87.4.5. Server Side
    87.5. Step-by-step Guide to Consumer Driven Contracts (CDC)
    87.5.1. Technical note
    87.5.2. Consumer side (Loan Issuance)
    87.5.3. Producer side (Fraud Detection server)
    87.5.4. Consumer Side (Loan Issuance) Final Step
    87.6. Dependencies
    87.7. Additional Links
    87.7.1. Spring Cloud Contract video
    87.7.2. Readings
    87.8. Samples
    88. Spring Cloud Contract FAQ
    88.1. Why use Spring Cloud Contract Verifier and not X ?
    88.2. I don’t want to write a contract in Groovy!
    88.3. What is this value(consumer(), producer()) ?
    88.4. How to do Stubs versioning?
    88.4.1. API Versioning
    88.4.2. JAR versioning
    88.4.3. Dev or prod stubs
    88.5. Common repo with contracts
    88.5.1. Repo structure
    88.5.2. Workflow
    88.5.3. Consumer
    88.5.4. Producer
    88.5.5. How can I define messaging contracts per topic not per producer?
    For Maven Project
    For Gradle Project
    88.6. Do I need a Binary Storage? Can’t I use Git?
    88.6.1. Protocol convention
    88.6.2. Producer
    88.6.3. Producer with contracts stored locally
    Keeping contracts with the producer and stubs in an external repository
    88.6.4. Consumer
    88.7. Can I use the Pact Broker?
    88.7.1. Pact Consumer
    88.7.2. Producer
    88.7.3. Pact Consumer (Producer Contract approach)
    88.8. How can I debug the request/response being sent by the generated tests client?
    88.8.1. How can I debug the mapping/request/response being sent by WireMock?
    88.8.2. How can I see what got registered in the HTTP server stub?
    88.8.3. Can I reference text from file?
    89. Spring Cloud Contract Verifier Setup
    89.1. Gradle Project
    89.1.1. Prerequisites
    90. Add Gradle Plugin with Dependencies
    90.1. Gradle and Rest Assured 2.0
    90.2. Snapshot Versions for Gradle
    90.3. Add stubs
    90.4. Run the Plugin
    90.5. Default Setup
    90.6. Configure Plugin
    90.7. Configuration Options
    90.8. Single Base Class for All Tests
    90.9. Different Base Classes for Contracts
    90.10. Invoking Generated Tests
    90.11. Pushing stubs to SCM
    90.12. Spring Cloud Contract Verifier on the Consumer Side
    90.13. Maven Project
    90.13.1. Add maven plugin
    90.13.2. Maven and Rest Assured 2.0
    90.13.3. Snapshot versions for Maven
    90.13.4. Add stubs
    90.13.5. Run plugin
    90.13.6. Configure plugin
    90.13.7. Configuration Options
    90.13.8. Single Base Class for All Tests
    90.13.9. Different base classes for contracts
    90.13.10. Invoking generated tests
    90.13.11. Pushing stubs to SCM
    90.13.12. Maven Plugin and STS
    90.13.13. Maven Plugin with Spock Tests
    90.14. Stubs and Transitive Dependencies
    90.15. Scenarios
    90.16. Docker Project
    90.16.1. Short intro to Maven, JARs and Binary storage
    90.16.2. How it works
    Environment Variables
    90.16.3. Example of usage
    90.16.4. Server side (nodejs)
    91. Spring Cloud Contract Verifier Messaging
    91.1. Integrations
    91.2. Manual Integration Testing
    91.3. Publisher-Side Test Generation
    91.3.1. Scenario 1: No Input Message
    91.3.2. Scenario 2: Output Triggered by Input
    91.3.3. Scenario 3: No Output Message
    91.4. Consumer Stub Generation
    92. Spring Cloud Contract Stub Runner
    92.1. Snapshot versions
    92.2. Publishing Stubs as JARs
    92.3. Stub Runner Core
    92.3.1. Retrieving stubs
    Stub downloading
    Classpath scanning
    Configuring HTTP Server Stubs
    92.3.2. Running stubs
    Running using main app
    HTTP Stubs
    Viewing registered mappings
    Messaging Stubs
    92.4. Stub Runner JUnit Rule and Stub Runner JUnit5 Extension
    92.4.1. Maven settings
    92.4.2. Providing fixed ports
    92.4.3. Fluent API
    92.4.4. Stub Runner with Spring
    92.5. Stub Runner Spring Cloud
    92.5.1. Stubbing Service Discovery
    Test profiles and service discovery
    92.5.2. Additional Configuration
    92.6. Stub Runner Boot Application
    92.6.1. How to use it?
    Stub Runner Server
    Stub Runner Server Fat Jar
    Spring Cloud CLI
    92.6.2. Endpoints
    HTTP
    Messaging
    92.6.3. Example
    92.6.4. Stub Runner Boot with Service Discovery
    92.7. Stubs Per Consumer
    92.8. Common
    92.8.1. Common Properties for JUnit and Spring
    92.8.2. Stub Runner Stubs IDs
    92.9. Stub Runner Docker
    92.9.1. How to use it
    92.9.2. Example of client side usage in a non JVM project
    93. Stub Runner for Messaging
    93.1. Stub triggering
    93.1.1. Trigger by Label
    93.1.2. Trigger by Group and Artifact Ids
    93.1.3. Trigger by Artifact Ids
    93.1.4. Trigger All Messages
    93.2. Stub Runner Camel
    93.2.1. Adding it to the project
    93.2.2. Disabling the functionality
    93.2.3. Examples
    Stubs structure
    Scenario 1 (no input message)
    Scenario 2 (output triggered by input)
    Scenario 3 (input with no output)
    93.3. Stub Runner Integration
    93.3.1. Adding the Runner to the Project
    93.3.2. Disabling the functionality
    Scenario 1 (no input message)
    Scenario 2 (output triggered by input)
    Scenario 3 (input with no output)
    93.4. Stub Runner Stream
    93.4.1. Adding the Runner to the Project
    93.4.2. Disabling the functionality
    Scenario 1 (no input message)
    Scenario 2 (output triggered by input)
    Scenario 3 (input with no output)
    93.5. Stub Runner Spring AMQP
    93.5.1. Adding the Runner to the Project
    Triggering the message
    Spring AMQP Test Configuration
    94. Contract DSL
    94.1. Limitations
    94.2. Common Top-Level elements
    94.2.1. Description
    94.2.2. Name
    94.2.3. Ignoring Contracts
    94.2.4. Passing Values from Files
    94.2.5. HTTP Top-Level Elements
    94.3. Request
    94.4. Response
    94.5. Dynamic properties
    94.5.1. Dynamic properties inside the body
    94.5.2. Regular expressions
    94.5.3. Passing Optional Parameters
    94.5.4. Executing Custom Methods on the Server Side
    94.5.5. Referencing the Request from the Response
    94.5.6. Registering Your Own WireMock Extension
    94.5.7. Dynamic Properties in the Matchers Sections
    94.6. JAX-RS Support
    94.7. Async Support
    94.8. Working with Context Paths
    94.9. Working with WebFlux
    94.9.1. WebFlux with WebTestClient
    94.9.2. WebFlux with Explicit mode
    94.10. XML Support for REST
    94.11. Messaging Top-Level Elements
    94.11.1. Output Triggered by a Method
    94.11.2. Output Triggered by a Message
    94.11.3. Consumer/Producer
    94.11.4. Common
    94.12. Multiple Contracts in One File
    94.13. Generating Spring REST Docs snippets from the contracts
    95. Customization
    95.1. Extending the DSL
    95.1.1. Common JAR
    95.1.2. Adding the Dependency to the Project
    95.1.3. Test the Dependency in the Project’s Dependencies
    95.1.4. Test a Dependency in the Plugin’s Dependencies
    95.1.5. Referencing classes in DSLs
    96. Using the Pluggable Architecture
    96.1. Custom Contract Converter
    96.1.1. Pact Converter
    96.1.2. Pact Contract
    96.1.3. Pact for Producers
    96.1.4. Pact for Consumers
    96.2. Using the Custom Test Generator
    96.3. Using the Custom Stub Generator
    96.4. Using the Custom Stub Runner
    96.5. Using the Custom Stub Downloader
    96.6. Using the SCM Stub Downloader
    96.7. Using the Pact Stub Downloader
    97. Spring Cloud Contract WireMock
    97.1. Registering Stubs Automatically
    97.2. Using Files to Specify the Stub Bodies
    97.3. Alternative: Using JUnit Rules
    97.4. Relaxed SSL Validation for Rest Template
    97.5. WireMock and Spring MVC Mocks
    97.6. Customization of WireMock configuration
    97.7. Generating Stubs using REST Docs
    97.8. Generating Contracts by Using REST Docs
    98. Migrations
    98.1. 1.0.x → 1.1.x
    98.1.1. New structure of generated stubs
    98.2. 1.1.x → 1.2.x
    98.2.1. Custom HttpServerStub
    98.2.2. New packages for generated tests
    98.2.3. New Methods in TemplateProcessor
    98.2.4. RestAssured 3.0
    98.3. 1.2.x → 2.0.x
    99. Links
    XIV. Spring Cloud Vault
    100. Quick Start
    101. Client Side Usage
    101.1. Authentication
    102. Authentication methods
    102.1. Token authentication
    102.2. AppId authentication
    102.2.1. Custom UserId
    102.3. AppRole authentication
    102.4. AWS-EC2 authentication
    102.5. AWS-IAM authentication
    102.6. Azure MSI authentication
    102.7. TLS certificate authentication
    102.8. Cubbyhole authentication
    102.9. GCP-GCE authentication
    102.10. GCP-IAM authentication
    102.11. Kubernetes authentication
    103. Secret Backends
    103.1. Generic Backend
    103.2. Versioned Key-Value Backend
    103.3. Consul
    103.4. RabbitMQ
    103.5. AWS
    104. Database backends
    104.1. Database
    104.2. Apache Cassandra
    104.3. MongoDB
    104.4. MySQL
    104.5. PostgreSQL
    105. Configure PropertySourceLocator behavior
    106. Service Registry Configuration
    107. Vault Client Fail Fast
    108. Vault Client SSL configuration
    109. Lease lifecycle management (renewal and revocation)
    XV. Spring Cloud Gateway
    110. How to Include Spring Cloud Gateway
    111. Glossary
    112. How It Works
    113. Configuring Route Predicate Factories and Gateway Filter Factories
    113.1. Shortcut Configuration
    113.2. Fully Expanded Arguments
    114. Route Predicate Factories
    114.1. After Route Predicate Factory
    114.2. Before Route Predicate Factory
    114.3. Between Route Predicate Factory
    114.4. Cookie Route Predicate Factory
    114.5. Header Route Predicate Factory
    114.6. Host Route Predicate Factory
    114.7. Method Route Predicate Factory
    114.8. Path Route Predicate Factory
    114.9. Query Route Predicate Factory
    114.10. RemoteAddr Route Predicate Factory
    114.11. Weight Route Predicate Factory
    114.11.1. Modifying the way remote addresses are resolved
    115. GatewayFilter Factories
    115.1. AddRequestHeader GatewayFilter Factory
    115.2. AddRequestParameter GatewayFilter Factory
    115.3. AddResponseHeader GatewayFilter Factory
    115.4. DedupeResponseHeader GatewayFilter Factory
    115.5. Hystrix GatewayFilter Factory
    115.6. FallbackHeaders GatewayFilter Factory
    115.7. MapRequestHeader GatewayFilter Factory
    115.8. PrefixPath GatewayFilter Factory
    115.9. PreserveHostHeader GatewayFilter Factory
    115.10. RequestRateLimiter GatewayFilter Factory
    115.10.1. Redis RateLimiter
    115.11. RedirectTo GatewayFilter Factory
    115.12. RemoveRequestHeader GatewayFilter Factory
    115.13. RemoveResponseHeader GatewayFilter Factory
    115.14. RewritePath GatewayFilter Factory
    115.15. RewriteLocationResponseHeader GatewayFilter Factory
    115.16. RewriteResponseHeader GatewayFilter Factory
    115.17. SaveSession GatewayFilter Factory
    115.18. SecureHeaders GatewayFilter Factory
    115.19. SetPath GatewayFilter Factory
    115.20. SetRequestHeader GatewayFilter Factory
    115.21. SetResponseHeader GatewayFilter Factory
    115.22. SetStatus GatewayFilter Factory
    115.23. StripPrefix GatewayFilter Factory
    115.24. Retry GatewayFilter Factory
    115.25. RequestSize GatewayFilter Factory
    115.26. Modify Request Body GatewayFilter Factory
    115.27. Modify Response Body GatewayFilter Factory
    115.28. Default Filters
    116. Global Filters
    116.1. Combined Global Filter and GatewayFilter Ordering
    116.2. Forward Routing Filter
    116.3. LoadBalancerClient Filter
    116.4. ReactiveLoadBalancerClientFilter
    116.5. Netty Routing Filter
    116.6. Netty Write Response Filter
    116.7. RouteToRequestUrl Filter
    116.8. Websocket Routing Filter
    116.9. Gateway Metrics Filter
    116.10. Marking An Exchange As Routed
    117. HttpHeadersFilters
    117.1. Forwarded Headers Filter
    117.2. RemoveHopByHop Headers Filter
    117.3. XForwarded Headers Filter
    118. TLS / SSL
    118.1. TLS Handshake
    119. Configuration
    119.1. Fluent Java Routes API
    119.2. DiscoveryClient Route Definition Locator
    119.2.1. Configuring Predicates and Filters For DiscoveryClient Routes
    120. Reactor Netty Access Logs
    121. CORS Configuration
    122. Actuator API
    122.1. Verbose Actuator Format
    122.2. Retrieving route filters
    122.2.1. Global Filters
    122.2.2. Route Filters
    122.3. Refreshing the route cache
    122.4. Retrieving the routes defined in the gateway
    122.5. Retrieving information about a particular route
    122.6. Creating and deleting a particular route
    122.7. Recap: list of all endpoints
    123. Troubleshooting
    123.1. Log Levels
    123.2. Wiretap
    124. Developer Guide
    124.1. Writing Custom Route Predicate Factories
    124.2. Writing Custom GatewayFilter Factories
    124.3. Writing Custom Global Filters
    125. Building a Simple Gateway Using Spring MVC or Webflux
    XVI. Spring Cloud Function
    126. Introduction
    127. Getting Started
    128. Building and Running a Function
    129. Function Catalog and Flexible Function Signatures
    129.1. Java 8 function support
    129.2. Kotlin Lambda support
    130. Standalone Web Applications
    131. Standalone Streaming Applications
    132. Deploying a Packaged Function
    133. Functional Bean Definitions
    133.1. Comparing Functional with Traditional Bean Definitions
    133.2. Testing Functional Applications
    133.3. Limitations of Functional Bean Declaration
    134. Dynamic Compilation
    135. Serverless Platform Adapters
    135.1. AWS Lambda
    135.1.1. Introduction
    135.1.2. Notes on JAR Layout
    135.1.3. Upload
    135.1.4. Platfom Specific Features
    HTTP and API Gateway
    135.2. Azure Functions
    135.2.1. Notes on JAR Layout
    135.2.2. Build
    135.2.3. Running the sample
    135.3. Apache Openwhisk
    135.3.1. Quick Start
    XVII. Spring Cloud Kubernetes
    136. Why do you need Spring Cloud Kubernetes?
    137. Starters
    138. DiscoveryClient for Kubernetes
    139. Kubernetes native service discovery
    140. Kubernetes PropertySource implementations
    140.1. Using a ConfigMap PropertySource
    140.2. Secrets PropertySource
    140.3. PropertySource Reload
    141. Ribbon Discovery in Kubernetes
    142. Kubernetes Ecosystem Awareness
    142.1. Kubernetes Profile Autoconfiguration
    142.2. Istio Awareness
    143. Pod Health Indicator
    144. Leader Election
    145. Security Configurations Inside Kubernetes
    145.1. Namespace
    145.2. Service Account
    146. Service Registry Implementation
    147. Examples
    148. Other Resources
    149. Building
    149.1. Basic Compile and Test
    149.2. Documentation
    149.3. Working with the code
    149.3.1. Importing into eclipse with m2eclipse
    149.3.2. Importing into eclipse without m2eclipse
    150. Contributing
    150.1. Sign the Contributor License Agreement
    150.2. Code of Conduct
    150.3. Code Conventions and Housekeeping
    150.4. Checkstyle
    150.4.1. Checkstyle configuration
    150.5. IDE setup
    150.5.1. Intellij IDEA
    XVIII. Spring Cloud GCP
    151. Introduction
    152. Dependency Management
    153. Getting started
    153.1. Spring Initializr
    153.1.1. GCP Support
    153.1.2. GCP Messaging
    153.1.3. GCP Storage
    153.2. Code Samples
    153.3. Code Challenges
    153.4. Getting Started Guides
    154. Spring Cloud GCP Core
    154.1. Project ID
    154.2. Credentials
    154.2.1. Scopes
    154.3. Environment
    154.4. Spring Initializr
    155. Google Cloud Pub/Sub
    155.1. Pub/Sub Operations & Template
    155.1.1. Publishing to a topic
    JSON support
    155.1.2. Subscribing to a subscription
    155.1.3. Pulling messages from a subscription
    155.2. Pub/Sub management
    155.2.1. Creating a topic
    155.2.2. Deleting a topic
    155.2.3. Listing topics
    155.2.4. Creating a subscription
    155.2.5. Deleting a subscription
    155.2.6. Listing subscriptions
    155.3. Configuration
    155.4. Sample
    156. Spring Resources
    156.1. Google Cloud Storage
    156.1.1. Setting the Content Type
    156.2. Configuration
    156.3. Sample
    157. Spring JDBC
    157.1. Prerequisites
    157.2. Spring Boot Starter for Google Cloud SQL
    157.2.1. DataSource creation flow
    157.2.2. Troubleshooting tips
    Connection issues
    Errors like c.g.cloud.sql.core.SslSocketFactory : Re-throwing cached exception due to attempt to refresh instance information too soon after error
    PostgreSQL: java.net.SocketException: already connected issue
    157.3. Samples
    158. Spring Integration
    158.1. Channel Adapters for Cloud Pub/Sub
    158.1.1. Inbound channel adapter
    158.1.2. Outbound channel adapter
    158.1.3. Header mapping
    158.2. Sample
    158.3. Channel Adapters for Google Cloud Storage
    158.3.1. Inbound channel adapter
    158.3.2. Inbound streaming channel adapter
    158.3.3. Outbound channel adapter
    158.4. Sample
    159. Spring Cloud Stream
    159.1. Overview
    159.2. Configuration
    159.2.1. Producer Destination Configuration
    159.2.2. Consumer Destination Configuration
    159.3. Sample
    160. Spring Cloud Sleuth
    160.1. Tracing
    160.2. Spring Boot Starter for Stackdriver Trace
    160.3. Overriding the auto-configuration
    160.4. Integration with Logging
    160.5. Sample
    161. Stackdriver Logging
    161.1. Web MVC Interceptor
    161.2. Logback Support
    161.2.1. Log via API
    161.2.2. Log via Console
    161.3. Sample
    162. Spring Cloud Config
    162.1. Configuration
    162.2. Quick start
    162.3. Refreshing the configuration at runtime
    162.4. Sample
    163. Spring Data Cloud Spanner
    163.1. Configuration
    163.1.1. Cloud Spanner settings
    163.1.2. Repository settings
    163.1.3. Autoconfiguration
    163.2. Object Mapping
    163.2.1. Constructors
    163.2.2. Table
    SpEL expressions for table names
    163.2.3. Primary Keys
    163.2.4. Columns
    163.2.5. Embedded Objects
    163.2.6. Relationships
    163.2.7. Supported Types
    163.2.8. Lists
    163.2.9. Lists of Structs
    163.2.10. Custom types
    163.2.11. Custom Converter for Struct Array Columns
    163.3. Spanner Operations & Template
    163.3.1. SQL Query
    163.3.2. Read
    163.3.3. Advanced reads
    Stale read
    Read from a secondary index
    Read with offsets and limits
    Sorting
    Partial read
    Summary of options for Query vs Read
    163.3.4. Write / Update
    Insert
    Update
    Upsert
    Partial Update
    163.3.5. DML
    163.3.6. Transactions
    Read/Write Transaction
    Read-only Transaction
    Declarative Transactions with @Transactional Annotation
    163.3.7. DML Statements
    163.4. Repositories
    163.4.1. CRUD Repository
    163.4.2. Paging and Sorting Repository
    163.4.3. Spanner Repository
    163.5. Query Methods
    163.5.1. Query methods by convention
    163.5.2. Custom SQL/DML query methods
    Query methods with named queries properties
    Query methods with annotation
    163.5.3. Projections
    163.5.4. REST Repositories
    163.6. Database and Schema Admin
    163.7. Sample
    164. Spring Data Cloud Datastore
    164.1. Configuration
    164.1.1. Cloud Datastore settings
    164.1.2. Repository settings
    164.1.3. Autoconfiguration
    164.2. Object Mapping
    164.2.1. Constructors
    164.2.2. Kind
    164.2.3. Keys
    164.2.4. Fields
    164.2.5. Supported Types
    164.2.6. Custom types
    164.2.7. Collections and arrays
    164.2.8. Custom Converter for collections
    164.3. Relationships
    164.3.1. Embedded Entities
    Maps
    164.3.2. Ancestor-Descendant Relationships
    164.3.3. Key Reference Relationships
    164.4. Datastore Operations & Template
    164.4.1. GQL Query
    164.4.2. Find by ID(s)
    Indexes
    Read with offsets, limits, and sorting
    Partial read
    164.4.3. Write / Update
    Partial Update
    164.4.4. Transactions
    Declarative Transactions with @Transactional Annotation
    164.4.5. Read-Write Support for Maps
    164.5. Repositories
    164.5.1. Query methods by convention
    164.5.2. Custom GQL query methods
    Query methods with annotation
    Query methods with named queries properties
    164.5.3. Transactions
    164.5.4. Projections
    164.5.5. REST Repositories
    164.6. Sample
    165. Cloud Memorystore for Redis
    165.1. Spring Caching
    166. Cloud Identity-Aware Proxy (IAP) Authentication
    166.1. Configuration
    166.2. Sample
    167. Google Cloud Vision
    167.1. Cloud Vision Template
    167.2. Detect Image Labels Example
    167.3. Sample
    168. Cloud Foundry
    169. Kotlin Support
    169.1. Prerequisites
    170. Sample
    XIX. Appendix: Compendium of Configuration Properties

    Spring Cloud provides tools for developers to quickly build some of +the common patterns in distributed systems (e.g. configuration +management, service discovery, circuit breakers, intelligent routing, +micro-proxy, control bus). Coordination of +distributed systems leads to boiler plate patterns, and using Spring +Cloud developers can quickly stand up services and applications that +implement those patterns. They will work well in any distributed +environment, including the developer’s own laptop, bare metal data +centres, and managed platforms such as Cloud Foundry.

    Version: Greenwich.SR5

    1. Features

    Spring Cloud focuses on providing good out of box experience for typical use cases +and extensibility mechanism to cover others.

    • Distributed/versioned configuration
    • Service registration and discovery
    • Routing
    • Service-to-service calls
    • Load balancing
    • Circuit Breakers
    • Distributed messaging

    Part I. Cloud Native Applications

    Cloud Native is a style of application development that encourages easy adoption of best practices in the areas of continuous delivery and value-driven development. +A related discipline is that of building 12-factor Applications, in which development practices are aligned with delivery and operations goals — for instance, by using declarative programming and management and monitoring. +Spring Cloud facilitates these styles of development in a number of specific ways. + The starting point is a set of features to which all components in a distributed system need easy access.

    Many of those features are covered by Spring Boot, on which Spring Cloud builds. Some more features are delivered by Spring Cloud as two libraries: Spring Cloud Context and Spring Cloud Commons. +Spring Cloud Context provides utilities and special services for the ApplicationContext of a Spring Cloud application (bootstrap context, encryption, refresh scope, and environment endpoints). Spring Cloud Commons is a set of abstractions and common classes used in different Spring Cloud implementations (such as Spring Cloud Netflix and Spring Cloud Consul).

    If you get an exception due to "Illegal key size" and you use Sun’s JDK, you need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. +See the following links for more information:

    Extract the files into the JDK/jre/lib/security folder for whichever version of JRE/JDK x64/x86 you use.

    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. +If you would like to contribute to this section of the documentation or if you find an error, you can find the source code and issue trackers for the project at github.

    2. Spring Cloud Context: Application Context Services

    Spring Boot has an opinionated view of how to build an application with Spring. +For instance, it has conventional locations for common configuration files and has endpoints for common management and monitoring tasks. +Spring Cloud builds on top of that and adds a few features that probably all components in a system would use or occasionally need.

    2.1 The Bootstrap Application Context

    A Spring Cloud application operates by creating a bootstrap context, which is a parent context for the main application. +It is responsible for loading configuration properties from the external sources and for decrypting properties in the local external configuration files. +The two contexts share an Environment, which is the source of external properties for any Spring application. +By default, bootstrap properties (not bootstrap.properties but properties that are loaded during the bootstrap phase) are added with high precedence, so they cannot be overridden by local configuration.

    The bootstrap context uses a different convention for locating external configuration than the main application context. +Instead of application.yml (or .properties), you can use bootstrap.yml, keeping the external configuration for bootstrap and main context +nicely separate. +The following listing shows an example:

    bootstrap.yml.  +

    spring:
    +  application:
    +    name: foo
    +  cloud:
    +    config:
    +      uri: ${SPRING_CONFIG_URI:http://localhost:8888}

    +

    If your application needs any application-specific configuration from the server, it is a good idea to set the spring.application.name (in bootstrap.yml or application.yml). +In order for the property spring.application.name to be used as the application’s context ID you +must set it in bootstrap.[properties | yml].

    If you want to retrieve specific profile configuration, you should also set spring.profiles.active in bootstrap.[properties | yml].

    You can disable the bootstrap process completely by setting spring.cloud.bootstrap.enabled=false (for example, in system properties).

    2.2 Application Context Hierarchies

    If you build an application context from SpringApplication or SpringApplicationBuilder, then the Bootstrap context is added as a parent to that context. +It is a feature of Spring that child contexts inherit property sources and profiles from their parent, so the main application context contains additional property sources, compared to building the same context without Spring Cloud Config. +The additional property sources are:

    • bootstrap: If any PropertySourceLocators are found in the Bootstrap context and if they have non-empty properties, an optional CompositePropertySource appears with high priority. +An example would be properties from the Spring Cloud Config Server. +See Section 2.6, “Customizing the Bootstrap Property Sources” for instructions on how to customize the contents of this property source.
    • applicationConfig: [classpath:bootstrap.yml] (and related files if Spring profiles are active): If you have a bootstrap.yml (or .properties), those properties are used to configure the Bootstrap context. +Then they get added to the child context when its parent is set. +They have lower precedence than the application.yml (or .properties) and any other property sources that are added to the child as a normal part of the process of creating a Spring Boot application. +See Section 2.3, “Changing the Location of Bootstrap Properties” for instructions on how to customize the contents of these property sources.

    Because of the ordering rules of property sources, the bootstrap entries take precedence. +However, note that these do not contain any data from bootstrap.yml, which has very low precedence but can be used to set defaults.

    You can extend the context hierarchy by setting the parent context of any ApplicationContext you create — for example, by using its own interface or with the SpringApplicationBuilder convenience methods (parent(), child() and sibling()). +The bootstrap context is the parent of the most senior ancestor that you create yourself. +Every context in the hierarchy has its own bootstrap (possibly empty) property source to avoid promoting values inadvertently from parents down to their descendants. +If there is a Config Server, every context in the hierarchy can also (in principle) have a different spring.application.name and, hence, a different remote property source. +Normal Spring application context behavior rules apply to property resolution: properties from a child context override those in +the parent, by name and also by property source name. +(If the child has a property source with the same name as the parent, the value from the parent is not included in the child).

    Note that the SpringApplicationBuilder lets you share an Environment amongst the whole hierarchy, but that is not the default. +Thus, sibling contexts, in particular, do not need to have the same profiles or property sources, even though they may share common values with their parent.

    2.3 Changing the Location of Bootstrap Properties

    The bootstrap.yml (or .properties) location can be specified by setting spring.cloud.bootstrap.name (default: bootstrap), spring.cloud.bootstrap.location (default: empty) or spring.cloud.bootstrap.additional-location (default: empty) — for example, in System properties. +Those properties behave like the spring.config.* variants with the same name. +With spring.cloud.bootstrap.location the default locations are replaced and only the specified ones are used. +To add locations to the list of default ones, spring.cloud.bootstrap.additional-location could be used. +In fact, they are used to set up the bootstrap ApplicationContext by setting those properties in its Environment. +If there is an active profile (from spring.profiles.active or through the Environment API in the +context you are building), properties in that profile get loaded as well, the same as in a regular Spring Boot app — for example, from bootstrap-development.properties for a development profile.

    2.4 Overriding the Values of Remote Properties

    The property sources that are added to your application by the bootstrap context are often remote (from example, from Spring Cloud Config Server). +By default, they cannot be overridden locally. +If you want to let your applications override the remote properties with their own System properties or config files, the remote property source has to grant it permission by setting spring.cloud.config.allowOverride=true (it does not work to set this locally). +Once that flag is set, two finer-grained settings control the location of the remote properties in relation to system properties and the application’s local configuration:

    • spring.cloud.config.overrideNone=true: Override from any local property source.
    • spring.cloud.config.overrideSystemProperties=false: Only system properties, command line arguments, and environment variables (but not the local config files) should override the remote settings.

    2.5 Customizing the Bootstrap Configuration

    The bootstrap context can be set to do anything you like by adding entries to /META-INF/spring.factories under a key named org.springframework.cloud.bootstrap.BootstrapConfiguration. +This holds a comma-separated list of Spring @Configuration classes that are used to create the context. +Any beans that you want to be available to the main application context for autowiring can be created here. +There is a special contract for @Beans of type ApplicationContextInitializer. +If you want to control the startup sequence, classes can be marked with an @Order annotation (the default order is last).

    [Warning]Warning

    When adding custom BootstrapConfiguration, be careful that the classes you add are not @ComponentScanned by mistake into your main application context, where they might not be needed. +Use a separate package name for boot configuration classes and make sure that name is not already covered by your @ComponentScan or @SpringBootApplication annotated configuration classes.

    The bootstrap process ends by injecting initializers into the main SpringApplication instance (which is the normal Spring Boot startup sequence, whether it is running as a standalone application or deployed in an application server). +First, a bootstrap context is created from the classes found in spring.factories. +Then, all @Beans of type ApplicationContextInitializer are added to the main SpringApplication before it is started.

    2.6 Customizing the Bootstrap Property Sources

    The default property source for external configuration added by the bootstrap process is the Spring Cloud Config Server, but you can add additional sources by adding beans of type PropertySourceLocator to the bootstrap context (through spring.factories). +For instance, you can insert additional properties from a different server or from a database.

    As an example, consider the following custom locator:

    @Configuration
    +public class CustomPropertySourceLocator implements PropertySourceLocator {
    +
    +    @Override
    +    public PropertySource<?> locate(Environment environment) {
    +        return new MapPropertySource("customProperty",
    +                Collections.<String, Object>singletonMap("property.from.sample.custom.source", "worked as intended"));
    +    }
    +
    +}

    The Environment that is passed in is the one for the ApplicationContext about to be created — in other words, the one for which we supply additional property sources for. +It already has its normal Spring Boot-provided property sources, so you can use those to locate a property source specific to this Environment (for example, by keying it on spring.application.name, as is done in the default Spring Cloud Config Server property source locator).

    If you create a jar with this class in it and then add a META-INF/spring.factories containing the following, the customProperty PropertySource appears in any application that includes that jar on its classpath:

    org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomPropertySourceLocator

    2.7 Logging Configuration

    If you are going to use Spring Boot to configure log settings than +you should place this configuration in `bootstrap.[yml | properties] +if you would like it to apply to all events.

    [Note]Note

    For Spring Cloud to initialize logging configuration properly you cannot use a custom prefix. For example, +using custom.loggin.logpath will not be recognized by Spring Cloud when initializing the logging system.

    2.8 Environment Changes

    The application listens for an EnvironmentChangeEvent and reacts to the change in a couple of standard ways (additional ApplicationListeners can be added as @Beans by the user in the normal way). +When an EnvironmentChangeEvent is observed, it has a list of key values that have changed, and the application uses those to:

    • Re-bind any @ConfigurationProperties beans in the context
    • Set the logger levels for any properties in logging.level.*

    Note that the Config Client does not, by default, poll for changes in the Environment. +Generally, we would not recommend that approach for detecting changes (although you could set it up with a +@Scheduled annotation). +If you have a scaled-out client application, it is better to broadcast the EnvironmentChangeEvent to all the instances instead of having them polling for changes (for example, by using the Spring Cloud Bus).

    The EnvironmentChangeEvent covers a large class of refresh use cases, as long as you can actually make a change to the Environment and publish the event. +Note that those APIs are public and part of core Spring). +You can verify that the changes are bound to @ConfigurationProperties beans by visiting the /configprops endpoint (a normal Spring Boot Actuator feature). +For instance, a DataSource can have its maxPoolSize changed at runtime (the default DataSource created by Spring Boot is an @ConfigurationProperties bean) and grow capacity dynamically. +Re-binding @ConfigurationProperties does not cover another large class of use cases, where you need more control over the refresh and where you need a change to be atomic over the whole ApplicationContext. +To address those concerns, we have @RefreshScope.

    2.9 Refresh Scope

    When there is a configuration change, a Spring @Bean that is marked as @RefreshScope gets special treatment. +This feature addresses the problem of stateful beans that only get their configuration injected when they are initialized. +For instance, if a DataSource has open connections when the database URL is changed via the Environment, you probably want the holders of those connections to be able to complete what they are doing. +Then, the next time something borrows a connection from the pool, it gets one with the new URL.

    Sometimes, it might even be mandatory to apply the @RefreshScope +annotation on some beans which can be only initialized once. If a bean +is "immutable", you will have to either annotate the bean with @RefreshScope +or specify the classname under the property key +spring.cloud.refresh.extra-refreshable.

    [Important]Important

    If you create a DataSource bean yourself and the implementation is a HikariDataSource, return the +most specific type, in this case HikariDataSource. Otherwise, you will need to set +spring.cloud.refresh.extra-refreshable=javax.sql.DataSource.

    Refresh scope beans are lazy proxies that initialize when they are used (that is, when a method is called), and the scope acts as a cache of initialized values. +To force a bean to re-initialize on the next method call, you must invalidate its cache entry.

    The RefreshScope is a bean in the context and has a public refreshAll() method to refresh all beans in the scope by clearing the target cache. +The /refresh endpoint exposes this functionality (over HTTP or JMX). +To refresh an individual bean by name, there is also a refresh(String) method.

    To expose the /refresh endpoint, you need to add following configuration to your application:

    management:
    +  endpoints:
    +    web:
    +      exposure:
    +        include: refresh
    [Note]Note

    @RefreshScope works (technically) on an @Configuration class, but it might lead to surprising behavior. +For example, it does not mean that all the @Beans defined in that class are themselves in @RefreshScope. +Specifically, anything that depends on those beans cannot rely on them being updated when a refresh is initiated, unless it is itself in @RefreshScope. +In that case, it is rebuilt on a refresh and its dependencies are re-injected. At that point, they are re-initialized from the refreshed @Configuration).

    2.10 Encryption and Decryption

    Spring Cloud has an Environment pre-processor for decrypting property values locally. +It follows the same rules as the Config Server and has the same external configuration through encrypt.*. +Thus, you can use encrypted values in the form of {cipher}* and, as long as there is a valid key, they are decrypted before the main application context gets the Environment settings. +To use the encryption features in an application, you need to include Spring Security RSA in your classpath (Maven co-ordinates: "org.springframework.security:spring-security-rsa"), and you also need the full strength JCE extensions in your JVM.

    If you get an exception due to "Illegal key size" and you use Sun’s JDK, you need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. +See the following links for more information:

    Extract the files into the JDK/jre/lib/security folder for whichever version of JRE/JDK x64/x86 you use.

    2.11 Endpoints

    For a Spring Boot Actuator application, some additional management endpoints are available. You can use:

    • POST to /actuator/env to update the Environment and rebind @ConfigurationProperties and log levels.
    • /actuator/refresh to re-load the boot strap context and refresh the @RefreshScope beans.
    • /actuator/restart to close the ApplicationContext and restart it (disabled by default).
    • /actuator/pause and /actuator/resume for calling the Lifecycle methods (stop() and start() on the ApplicationContext).
    [Note]Note

    If you disable the /actuator/restart endpoint then the /actuator/pause and /actuator/resume endpoints +will also be disabled since they are just a special case of /actuator/restart.

    3. Spring Cloud Commons: Common Abstractions

    Patterns such as service discovery, load balancing, and circuit breakers lend themselves to a common abstraction layer that can be consumed by all Spring Cloud clients, independent of the implementation (for example, discovery with Eureka or Consul).

    3.1 @EnableDiscoveryClient

    Spring Cloud Commons provides the @EnableDiscoveryClient annotation. +This looks for implementations of the DiscoveryClient interface with META-INF/spring.factories. +Implementations of the Discovery Client add a configuration class to spring.factories under the org.springframework.cloud.client.discovery.EnableDiscoveryClient key. +Examples of DiscoveryClient implementations include Spring Cloud Netflix Eureka, Spring Cloud Consul Discovery, and Spring Cloud Zookeeper Discovery.

    By default, implementations of DiscoveryClient auto-register the local Spring Boot server with the remote discovery server. +This behavior can be disabled by setting autoRegister=false in @EnableDiscoveryClient.

    [Note]Note

    @EnableDiscoveryClient is no longer required. +You can put a DiscoveryClient implementation on the classpath to cause the Spring Boot application to register with the service discovery server.

    3.1.1 Health Indicator

    Commons creates a Spring Boot HealthIndicator that DiscoveryClient implementations can participate in by implementing DiscoveryHealthIndicator. +To disable the composite HealthIndicator, set spring.cloud.discovery.client.composite-indicator.enabled=false. +A generic HealthIndicator based on DiscoveryClient is auto-configured (DiscoveryClientHealthIndicator). +To disable it, set spring.cloud.discovery.client.health-indicator.enabled=false. +To disable the description field of the DiscoveryClientHealthIndicator, set spring.cloud.discovery.client.health-indicator.include-description=false. +Otherwise, it can bubble up as the description of the rolled up HealthIndicator.

    3.1.2 Ordering DiscoveryClient instances

    DiscoveryClient interface extends Ordered. This is useful when using multiple discovery + clients, as it allows you to define the order of the returned discovery clients, similar to +how you can order the beans loaded by a Spring application. By default, the order of any DiscoveryClient is set to +0. If you want to set a different order for your custom DiscoveryClient implementations, you just need to override +the getOrder() method so that it returns the value that is suitable for your setup. Apart from this, you can use +properties to set the order of the DiscoveryClient +implementations provided by Spring Cloud, among others ConsulDiscoveryClient, EurekaDiscoveryClient and +ZookeeperDiscoveryClient. In order to do it, you just need to set the +spring.cloud.{clientIdentifier}.discovery.order (or eureka.client.order for Eureka) property to the desired value.

    3.2 ServiceRegistry

    Commons now provides a ServiceRegistry interface that provides methods such as register(Registration) and deregister(Registration), which let you provide custom registered services. +Registration is a marker interface.

    The following example shows the ServiceRegistry in use:

    @Configuration
    +@EnableDiscoveryClient(autoRegister=false)
    +public class MyConfiguration {
    +    private ServiceRegistry registry;
    +
    +    public MyConfiguration(ServiceRegistry registry) {
    +        this.registry = registry;
    +    }
    +
    +    // called through some external process, such as an event or a custom actuator endpoint
    +    public void register() {
    +        Registration registration = constructRegistration();
    +        this.registry.register(registration);
    +    }
    +}

    Each ServiceRegistry implementation has its own Registry implementation.

    • ZookeeperRegistration used with ZookeeperServiceRegistry
    • EurekaRegistration used with EurekaServiceRegistry
    • ConsulRegistration used with ConsulServiceRegistry

    If you are using the ServiceRegistry interface, you are going to need to pass the +correct Registry implementation for the ServiceRegistry implementation you +are using.

    3.2.1 ServiceRegistry Auto-Registration

    By default, the ServiceRegistry implementation auto-registers the running service. +To disable that behavior, you can set: +* @EnableDiscoveryClient(autoRegister=false) to permanently disable auto-registration. +* spring.cloud.service-registry.auto-registration.enabled=false to disable the behavior through configuration.

    ServiceRegistry Auto-Registration Events

    There are two events that will be fired when a service auto-registers. The first event, called +InstancePreRegisteredEvent, is fired before the service is registered. The second +event, called InstanceRegisteredEvent, is fired after the service is registered. You can register an +ApplicationListener(s) to listen to and react to these events.

    [Note]Note

    These events will not be fired if spring.cloud.service-registry.auto-registration.enabled is set to false.

    3.2.2 Service Registry Actuator Endpoint

    Spring Cloud Commons provides a /service-registry actuator endpoint. +This endpoint relies on a Registration bean in the Spring Application Context. +Calling /service-registry with GET returns the status of the Registration. +Using POST to the same endpoint with a JSON body changes the status of the current Registration to the new value. +The JSON body has to include the status field with the preferred value. +Please see the documentation of the ServiceRegistry implementation you use for the allowed values when updating the status and the values returned for the status. +For instance, Eureka’s supported statuses are UP, DOWN, OUT_OF_SERVICE, and UNKNOWN.

    3.3 Spring RestTemplate as a Load Balancer Client

    RestTemplate can be automatically configured to use a Load-balancer client under the hood. +To create a load-balanced RestTemplate, create a RestTemplate @Bean and use the @LoadBalanced qualifier, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +    @LoadBalanced
    +    @Bean
    +    RestTemplate restTemplate() {
    +        return new RestTemplate();
    +    }
    +}
    +
    +public class MyClass {
    +    @Autowired
    +    private RestTemplate restTemplate;
    +
    +    public String doOtherStuff() {
    +        String results = restTemplate.getForObject("http://stores/stores", String.class);
    +        return results;
    +    }
    +}
    [Caution]Caution

    A RestTemplate bean is no longer created through auto-configuration. +Individual applications must create it.

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The Ribbon client is used to create a full physical address. +See RibbonAutoConfiguration for details of how the RestTemplate is set up.

    [Important]Important

    In order to use a load-balanced RestTemplate, you need to have a load-balancer implementation in your classpath. +The recommended implementation is BlockingLoadBalancerClient +- add org.springframework.cloud:spring-cloud-loadbalancer in order to use it. +The +RibbonLoadBalancerClient also can be used, but it’s now under maintenance and we do not recommend adding it to new projects.

    [Warning]Warning

    If you want to use BlockingLoadBalancerClient, make sure you do not have +RibbonLoadBalancerClient in the project classpath, as for backward compatibility reasons, it will be used by default.

    3.4 Spring WebClient as a Load Balancer Client

    WebClient can be automatically configured to use a load-balancer client. +To create a load-balanced WebClient, create a WebClient.Builder @Bean and use the @LoadBalanced qualifier, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +	@Bean
    +	@LoadBalanced
    +	public WebClient.Builder loadBalancedWebClientBuilder() {
    +		return WebClient.builder();
    +	}
    +}
    +
    +public class MyClass {
    +    @Autowired
    +    private WebClient.Builder webClientBuilder;
    +
    +    public Mono<String> doOtherStuff() {
    +        return webClientBuilder.build().get().uri("http://stores/stores")
    +        				.retrieve().bodyToMono(String.class);
    +    }
    +}

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The Ribbon client is used to create a full physical address.

    [Important]Important

    If you want to use a @LoadBalanced WebClient.Builder, you need to have a loadbalancer +implementation in the classpath. It is recommended that you add the +org.springframework.cloud:spring-cloud-loadbalancer dependency to your project. +Then, ReactiveLoadBalancer will be used underneath. +Alternatively, this functionality will also work with spring-cloud-starter-netflix-ribbon, but the request +will be handled by a non-reactive LoadBalancerClient under the hood. Additionally, +spring-cloud-starter-netflix-ribbon is already in maintenance mode, so we do not recommned +adding it to new projects.

    [Tip]Tip

    The ReactorLoadBalancer used underneath supports caching. If cacheManager is detected, +cached version of ServiceInstanceSupplier will be used. If not, we will retrieve instances +from discovery service without caching them. We recommend enabling caching in your project +if you use ReactiveLoadBalancer.

    3.4.1 Retrying Failed Requests

    A load-balanced RestTemplate can be configured to retry failed requests. +By default, this logic is disabled. +You can enable it by adding Spring Retry to your application’s classpath. +The load-balanced RestTemplate honors some of the Ribbon configuration values related to retrying failed requests. +You can use client.ribbon.MaxAutoRetries, client.ribbon.MaxAutoRetriesNextServer, and client.ribbon.OkToRetryOnAllOperations properties. +If you would like to disable the retry logic with Spring Retry on the classpath, you can set spring.cloud.loadbalancer.retry.enabled=false. +See the Ribbon documentation for a description of what these properties do.

    If you would like to implement a BackOffPolicy in your retries, you need to create a bean of type LoadBalancedRetryFactory and override the createBackOffPolicy method:

    @Configuration
    +public class MyConfiguration {
    +    @Bean
    +    LoadBalancedRetryFactory retryFactory() {
    +        return new LoadBalancedRetryFactory() {
    +            @Override
    +            public BackOffPolicy createBackOffPolicy(String service) {
    +        		return new ExponentialBackOffPolicy();
    +        	}
    +        };
    +    }
    +}
    [Note]Note

    client in the preceding examples should be replaced with your Ribbon client’s name.

    If you want to add one or more RetryListener implementations to your retry functionality, you need to +create a bean of type LoadBalancedRetryListenerFactory and return the RetryListener array +you would like to use for a given service, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +    @Bean
    +    LoadBalancedRetryListenerFactory retryListenerFactory() {
    +        return new LoadBalancedRetryListenerFactory() {
    +            @Override
    +            public RetryListener[] createRetryListeners(String service) {
    +                return new RetryListener[]{new RetryListener() {
    +                    @Override
    +                    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
    +                        //TODO Do you business...
    +                        return true;
    +                    }
    +
    +                    @Override
    +                     public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
    +                        //TODO Do you business...
    +                    }
    +
    +                    @Override
    +                    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
    +                        //TODO Do you business...
    +                    }
    +                }};
    +            }
    +        };
    +    }
    +}

    3.5 Multiple RestTemplate objects

    If you want a RestTemplate that is not load-balanced, create a RestTemplate bean and inject it. +To access the load-balanced RestTemplate, use the @LoadBalanced qualifier when you create your @Bean, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +    @LoadBalanced
    +    @Bean
    +    RestTemplate loadBalanced() {
    +        return new RestTemplate();
    +    }
    +
    +    @Primary
    +    @Bean
    +    RestTemplate restTemplate() {
    +        return new RestTemplate();
    +    }
    +}
    +
    +public class MyClass {
    +@Autowired
    +private RestTemplate restTemplate;
    +
    +    @Autowired
    +    @LoadBalanced
    +    private RestTemplate loadBalanced;
    +
    +    public String doOtherStuff() {
    +        return loadBalanced.getForObject("http://stores/stores", String.class);
    +    }
    +
    +    public String doStuff() {
    +        return restTemplate.getForObject("https://example.com", String.class);
    +    }
    +}
    [Important]Important

    Notice the use of the @Primary annotation on the plain RestTemplate declaration in the preceding example to disambiguate the unqualified @Autowired injection.

    [Tip]Tip

    If you see errors such as java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89, try injecting RestOperations or setting spring.aop.proxyTargetClass=true.

    3.6 Multiple WebClient Objects

    If you want a WebClient that is not load-balanced, create a WebClient bean and inject it. +To access the load-balanced WebClient, use the @LoadBalanced qualifier when you create your @Bean, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +
    +    @LoadBalanced
    +    @Bean
    +    WebClient.Builder loadBalanced() {
    +        return WebClient.builder();
    +    }
    +
    +    @Primary
    +    @Bean
    +    WebClient.Builder webClient() {
    +        return WebClient.builder();
    +    }
    +}
    +
    +public class MyClass {
    +    @Autowired
    +    private WebClient.Builder webClientBuilder;
    +
    +    @Autowired
    +    @LoadBalanced
    +    private WebClient.Builder loadBalanced;
    +
    +    public Mono<String> doOtherStuff() {
    +        return loadBalanced.build().get().uri("http://stores/stores")
    +        				.retrieve().bodyToMono(String.class);
    +    }
    +
    +    public Mono<String> doStuff() {
    +        return webClientBuilder.build().get().uri("http://example.com")
    +        				.retrieve().bodyToMono(String.class);
    +    }
    +}

    3.7 Spring WebFlux WebClient as a Load Balancer Client

    3.7.1 Spring WebFlux WebClient with Reactive Load Balancer

    WebClient can be configured to use the ReactiveLoadBalancer. +If you add org.springframework.cloud:spring-cloud-loadbalancer to your project, + ReactorLoadBalancerExchangeFilterFunction is auto-configured if spring-webflux is on the classpath. +The following example shows how to configure a WebClient to use reactive load balancer under the hood:

    public class MyClass {
    +    @Autowired
    +    private ReactorLoadBalancerExchangeFilterFunction lbFunction;
    +
    +    public Mono<String> doOtherStuff() {
    +        return WebClient.builder().baseUrl("http://stores")
    +            .filter(lbFunction)
    +            .build()
    +            .get()
    +            .uri("/stores")
    +            .retrieve()
    +            .bodyToMono(String.class);
    +    }
    +}

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The ReactorLoadBalancerClient is used to create a full physical address.

    3.7.2 Spring WebFlux WebClient with non-reactive Load Balancer Client

    If you you don’t have org.springframework.cloud:spring-cloud-loadbalancer in your project, +but you do have spring-cloud-starter-netflix-ribbon, you can still use WebClient with LoadBalancerClient. LoadBalancerExchangeFilterFunction +will be auto-configured if spring-webflux is on the classpath. Please note, however, that this is +uses a non-reactive client under the hood. +The following example shows how to configure a WebClient to use load balancer:

    public class MyClass {
    +    @Autowired
    +    private LoadBalancerExchangeFilterFunction lbFunction;
    +
    +    public Mono<String> doOtherStuff() {
    +        return WebClient.builder().baseUrl("http://stores")
    +            .filter(lbFunction)
    +            .build()
    +            .get()
    +            .uri("/stores")
    +            .retrieve()
    +            .bodyToMono(String.class);
    +    }
    +}

    The URI needs to use a virtual host name (that is, a service name, not a host name). +The LoadBalancerClient is used to create a full physical address.

    WARN: This approach is now deprecated. +We suggest you use WebFlux with reactive Load-Balancer +instead.

    3.7.3 Passing your own Load-Balancer Client configuration

    You can also use the @LoadBalancerClient annotation to pass your own load-balancer client configuration, passing the name of the load-balancer client and the configuration class, like so:

    @Configuration
    +@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class)
    +public class MyConfiguration {
    +
    +	@Bean
    +	@LoadBalanced
    +	public WebClient.Builder loadBalancedWebClientBuilder() {
    +		return WebClient.builder();
    +	}
    +}

    It is also possible to pass together multiple configurations (for more than one load-balancer client) via the @LoadBalancerClients annotation, as shown below:

    @Configuration
    +@LoadBalancerClients({@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class), @LoadBalancerClient(value = "customers", configuration = CustomersLoadBalancerClientConfiguration.class)})
    +public class MyConfiguration {
    +
    +	@Bean
    +	@LoadBalanced
    +	public WebClient.Builder loadBalancedWebClientBuilder() {
    +		return WebClient.builder();
    +	}
    +}

    3.8 Ignore Network Interfaces

    Sometimes, it is useful to ignore certain named network interfaces so that they can be excluded from Service Discovery registration (for example, when running in a Docker container). +A list of regular expressions can be set to cause the desired network interfaces to be ignored. +The following configuration ignores the docker0 interface and all interfaces that start with veth:

    application.yml.  +

    spring:
    +  cloud:
    +    inetutils:
    +      ignoredInterfaces:
    +        - docker0
    +        - veth.*

    +

    You can also force the use of only specified network addresses by using a list of regular expressions, as shown in the following example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    inetutils:
    +      preferredNetworks:
    +        - 192.168
    +        - 10.0

    +

    You can also force the use of only site-local addresses, as shown in the following example: +.application.yml

    spring:
    +  cloud:
    +    inetutils:
    +      useOnlySiteLocalInterfaces: true

    See Inet4Address.html.isSiteLocalAddress() for more details about what constitutes a site-local address.

    3.9 HTTP Client Factories

    Spring Cloud Commons provides beans for creating both Apache HTTP clients (ApacheHttpClientFactory) and OK HTTP clients (OkHttpClientFactory). +The OkHttpClientFactory bean is created only if the OK HTTP jar is on the classpath. +In addition, Spring Cloud Commons provides beans for creating the connection managers used by both clients: ApacheHttpClientConnectionManagerFactory for the Apache HTTP client and OkHttpClientConnectionPoolFactory for the OK HTTP client. +If you would like to customize how the HTTP clients are created in downstream projects, you can provide your own implementation of these beans. +In addition, if you provide a bean of type HttpClientBuilder or OkHttpClient.Builder, the default factories use these builders as the basis for the builders returned to downstream projects. +You can also disable the creation of these beans by setting spring.cloud.httpclientfactories.apache.enabled or spring.cloud.httpclientfactories.ok.enabled to false.

    3.10 Enabled Features

    Spring Cloud Commons provides a /features actuator endpoint. +This endpoint returns features available on the classpath and whether they are enabled. +The information returned includes the feature type, name, version, and vendor.

    3.10.1 Feature types

    There are two types of 'features': abstract and named.

    Abstract features are features where an interface or abstract class is defined and that an implementation the creates, such as DiscoveryClient, LoadBalancerClient, or LockService. +The abstract class or interface is used to find a bean of that type in the context. +The version displayed is bean.getClass().getPackage().getImplementationVersion().

    Named features are features that do not have a particular class they implement, such as "Circuit Breaker", "API Gateway", "Spring Cloud Bus", and others. These features require a name and a bean type.

    3.10.2 Declaring features

    Any module can declare any number of HasFeature beans, as shown in the following examples:

    @Bean
    +public HasFeatures commonsFeatures() {
    +  return HasFeatures.abstractFeatures(DiscoveryClient.class, LoadBalancerClient.class);
    +}
    +
    +@Bean
    +public HasFeatures consulFeatures() {
    +  return HasFeatures.namedFeatures(
    +    new NamedFeature("Spring Cloud Bus", ConsulBusAutoConfiguration.class),
    +    new NamedFeature("Circuit Breaker", HystrixCommandAspect.class));
    +}
    +
    +@Bean
    +HasFeatures localFeatures() {
    +  return HasFeatures.builder()
    +      .abstractFeature(Foo.class)
    +      .namedFeature(new NamedFeature("Bar Feature", Bar.class))
    +      .abstractFeature(Baz.class)
    +      .build();
    +}

    Each of these beans should go in an appropriately guarded @Configuration.

    3.11 Spring Cloud Compatibility Verification

    Due to the fact that some users have problem with setting up Spring Cloud application, we’ve decided +to add a compatibility verification mechanism. It will break if your current setup is not compatible +with Spring Cloud requirements, together with a report, showing what exactly went wrong.

    At the moment we verify which version of Spring Boot is added to your classpath.

    Example of a report

    ***************************
    +APPLICATION FAILED TO START
    +***************************
    +
    +Description:
    +
    +Your project setup is incompatible with our requirements due to following reasons:
    +
    +- Spring Boot [2.1.0.RELEASE] is not compatible with this Spring Cloud release train
    +
    +
    +Action:
    +
    +Consider applying the following actions:
    +
    +- Change Spring Boot version to one of the following versions [1.2.x, 1.3.x] .
    +You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn].
    +If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section.

    In order to disable this feature, set spring.cloud.compatibility-verifier.enabled to false. +If you want to override the compatible Spring Boot versions, just set the +spring.cloud.compatibility-verifier.compatible-boot-versions property with a comma separated list +of compatible Spring Boot versions.

    Part II. Spring Cloud Config

    Greenwich.SR5

    Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments. +The concepts on both client and server map identically to the Spring Environment and PropertySource abstractions, so they fit very well with Spring applications but can be used with any application running in any language. +As an application moves through the deployment pipeline from dev to test and into production, you can manage the configuration between those environments and be certain that applications have everything they need to run when they migrate. +The default implementation of the server storage backend uses git, so it easily supports labelled versions of configuration environments as well as being accessible to a wide range of tooling for managing the content. +It is easy to add alternative implementations and plug them in with Spring configuration.

    4. Quick Start

    This quick start walks through using both the server and the client of Spring Cloud Config Server.

    First, start the server, as follows:

    $ cd spring-cloud-config-server
    +$ ../mvnw spring-boot:run

    The server is a Spring Boot application, so you can run it from your IDE if you prefer to do so (the main class is ConfigServerApplication).

    Next try out a client, as follows:

    $ curl localhost:8888/foo/development
    +{"name":"foo","label":"master","propertySources":[
    +  {"name":"https://github.com/scratches/config-repo/foo-development.properties","source":{"bar":"spam"}},
    +  {"name":"https://github.com/scratches/config-repo/foo.properties","source":{"foo":"bar"}}
    +]}

    The default strategy for locating property sources is to clone a git repository (at spring.cloud.config.server.git.uri) and use it to initialize a mini SpringApplication. +The mini-application’s Environment is used to enumerate property sources and publish them at a JSON endpoint.

    The HTTP service has resources in the following form:

    /{application}/{profile}[/{label}]
    +/{application}-{profile}.yml
    +/{label}/{application}-{profile}.yml
    +/{application}-{profile}.properties
    +/{label}/{application}-{profile}.properties

    where application is injected as the spring.config.name in the SpringApplication (what is normally application in a regular Spring Boot app), profile is an active profile (or comma-separated list of properties), and label is an optional git label (defaults to master.)

    Spring Cloud Config Server pulls configuration for remote clients from various sources. The following example gets configuration from a git repository (which must be provided), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo

    Other sources are any JDBC compatible database, Subversion, Hashicorp Vault, Credhub and local filesystems.

    4.1 Client Side Usage

    To use these features in an application, you can build it as a Spring Boot application that depends on spring-cloud-config-client (for an example, see the test cases for the config-client or the sample application). +The most convenient way to add the dependency is with a Spring Boot starter org.springframework.cloud:spring-cloud-starter-config. +There is also a parent pom and BOM (spring-cloud-starter-parent) for Maven users and a Spring IO version management properties file for Gradle and Spring CLI users. The following example shows a typical Maven configuration:

    pom.xml.  +

       <parent>
    +       <groupId>org.springframework.boot</groupId>
    +       <artifactId>spring-boot-starter-parent</artifactId>
    +       <version>{spring-boot-docs-version}</version>
    +       <relativePath /> <!-- lookup parent from repository -->
    +   </parent>
    +
    +<dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>{spring-cloud-version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>
    +
    +<dependencies>
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-starter-config</artifactId>
    +	</dependency>
    +	<dependency>
    +		<groupId>org.springframework.boot</groupId>
    +		<artifactId>spring-boot-starter-test</artifactId>
    +		<scope>test</scope>
    +	</dependency>
    +</dependencies>
    +
    +<build>
    +	<plugins>
    +           <plugin>
    +               <groupId>org.springframework.boot</groupId>
    +               <artifactId>spring-boot-maven-plugin</artifactId>
    +           </plugin>
    +	</plugins>
    +</build>
    +
    +   <!-- repositories also needed for snapshots and milestones -->

    +

    Now you can create a standard Spring Boot application, such as the following HTTP server:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello World!";
    +    }
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(Application.class, args);
    +    }
    +
    +}

    When this HTTP server runs, it picks up the external configuration from the default local config server (if it is running) on port 8888. +To modify the startup behavior, you can change the location of the config server by using bootstrap.properties (similar to application.properties but for the bootstrap phase of an application context), as shown in the following example:

    spring.cloud.config.uri: http://myconfigserver.com

    By default, if no application name is set, application will be used. To modify the name, the following property can be added to the bootstrap.properties file:

    spring.application.name: myapp
    [Note]Note

    When setting the property ${spring.application.name} do not prefix your app name with the reserved word application- to prevent issues resolving the correct property source.

    The bootstrap properties show up in the /env endpoint as a high-priority property source, as shown in the following example.

    $ curl localhost:8080/env
    +{
    +  "profiles":[],
    +  "configService:https://github.com/spring-cloud-samples/config-repo/bar.properties":{"foo":"bar"},
    +  "servletContextInitParams":{},
    +  "systemProperties":{...},
    +  ...
    +}

    A property source called ``configService:<URL of remote repository>/<file name> contains the foo property with a value of bar and is highest priority.

    [Note]Note

    The URL in the property source name is the git repository, not the config server URL.

    5. Spring Cloud Config Server

    Spring Cloud Config Server provides an HTTP resource-based API for external configuration (name-value pairs or equivalent YAML content). +The server is embeddable in a Spring Boot application, by using the @EnableConfigServer annotation. +Consequently, the following application is a config server:

    ConfigServer.java.  +

    @SpringBootApplication
    +@EnableConfigServer
    +public class ConfigServer {
    +  public static void main(String[] args) {
    +    SpringApplication.run(ConfigServer.class, args);
    +  }
    +}

    +

    Like all Spring Boot applications, it runs on port 8080 by default, but you can switch it to the more conventional port 8888 in various ways. +The easiest, which also sets a default configuration repository, is by launching it with spring.config.name=configserver (there is a configserver.yml in the Config Server jar). +Another is to use your own application.properties, as shown in the following example:

    application.properties.  +

    server.port: 8888
    +spring.cloud.config.server.git.uri: file://${user.home}/config-repo

    +

    where ${user.home}/config-repo is a git repository containing YAML and properties files.

    [Note]Note

    On Windows, you need an extra "/" in the file URL if it is absolute with a drive prefix (for example,file:///${user.home}/config-repo).

    [Tip]Tip

    The following listing shows a recipe for creating the git repository in the preceding example:

    $ cd $HOME
    +$ mkdir config-repo
    +$ cd config-repo
    +$ git init .
    +$ echo info.foo: bar > application.properties
    +$ git add -A .
    +$ git commit -m "Add application.properties"
    [Warning]Warning

    Using the local filesystem for your git repository is intended for testing only. +You should use a server to host your configuration repositories in production.

    [Warning]Warning

    The initial clone of your configuration repository can be quick and efficient if you keep only text files in it. +If you store binary files, especially large ones, you may experience delays on the first request for configuration or encounter out of memory errors in the server.

    5.1 Environment Repository

    Where should you store the configuration data for the Config Server? +The strategy that governs this behaviour is the EnvironmentRepository, serving Environment objects. +This Environment is a shallow copy of the domain from the Spring Environment (including propertySources as the main feature). +The Environment resources are parametrized by three variables:

    • {application}, which maps to spring.application.name on the client side.
    • {profile}, which maps to spring.profiles.active on the client (comma-separated list).
    • {label}, which is a server side feature labelling a "versioned" set of config files.

    Repository implementations generally behave like a Spring Boot application, loading configuration files from a spring.config.name equal to the {application} parameter, and spring.profiles.active equal to the {profiles} parameter. +Precedence rules for profiles are also the same as in a regular Spring Boot application: Active profiles take precedence over defaults, and, if there are multiple profiles, the last one wins (similar to adding entries to a Map).

    The following sample client application has this bootstrap configuration:

    bootstrap.yml.  +

    spring:
    +  application:
    +    name: foo
    +  profiles:
    +    active: dev,mysql

    +

    (As usual with a Spring Boot application, these properties could also be set by environment variables or command line arguments).

    If the repository is file-based, the server creates an +Environment from application.yml (shared between all clients) and +foo.yml (with foo.yml taking precedence). +If the YAML files have documents inside them that point to Spring profiles, those are applied with higher precedence (in order of the profiles listed). +If there are profile-specific YAML (or properties) files, these are also applied with higher precedence than the defaults. +Higher precedence translates to a PropertySource listed earlier in the Environment. +(These same rules apply in a standalone Spring Boot application.)

    You can set spring.cloud.config.server.accept-empty to false so that Server would return a HTTP 404 status, if the application is not found.By default, this flag is set to true.

    5.1.1 Git Backend

    The default implementation of EnvironmentRepository uses a Git backend, which is very convenient for managing upgrades and physical environments and for auditing changes. +To change the location of the repository, you can set the spring.cloud.config.server.git.uri configuration property in the Config Server (for example in application.yml). +If you set it with a file: prefix, it should work from a local repository so that you can get started quickly and easily without a server. However, in that case, the server operates directly on the local repository without cloning it (it does not matter if it is not bare because the Config Server never makes changes to the "remote" repository). +To scale the Config Server up and make it highly available, you need to have all instances of the server pointing to the same repository, so only a shared file system would work. +Even in that case, it is better to use the ssh: protocol for a shared filesystem repository, so that the server can clone it and use a local working copy as a cache.

    This repository implementation maps the {label} parameter of the HTTP resource to a git label (commit id, branch name, or tag). +If the git branch or tag name contains a slash (/), then the label in the HTTP URL should instead be specified with the special string (_) (to avoid ambiguity with other URL paths). +For example, if the label is foo/bar, replacing the slash would result in the following label: foo(_)bar. +The inclusion of the special string (_) can also be applied to the {application} parameter. +If you use a command-line client such as curl, be careful with the brackets in the URL — you should escape them from the shell with single quotes ('').

    Skipping SSL Certificate Validation

    The configuration server’s validation of the Git server’s SSL certificate can be disabled by setting the git.skipSslValidation property to true (default is false).

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://example.com/my/repo
    +          skipSslValidation: true

    Setting HTTP Connection Timeout

    You can configure the time, in seconds, that the configuration server will wait to acquire an HTTP connection. Use the git.timeout property.

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://example.com/my/repo
    +          timeout: 4

    Placeholders in Git URI

    Spring Cloud Config Server supports a git repository URL with placeholders for the {application} and {profile} (and {label} if you need it, but remember that the label is applied as a git label anyway). +So you can support a one repository per application policy by using a structure similar to the following:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/myorg/{application}

    You can also support a one repository per profile policy by using a similar pattern but with +{profile}.

    Additionally, using the special string "(_)" within your {application} parameters can enable support for multiple +organizations, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/{application}

    where {application} is provided at request time in the following format: organization(_)application.

    Pattern Matching and Multiple Repositories

    Spring Cloud Config also includes support for more complex requirements with pattern +matching on the application and profile name. +The pattern format is a comma-separated list of {application}/{profile} names with wildcards (note that a pattern beginning with a wildcard may need to be quoted), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          repos:
    +            simple: https://github.com/simple/config-repo
    +            special:
    +              pattern: special*/dev*,*special*/dev*
    +              uri: https://github.com/special/config-repo
    +            local:
    +              pattern: local*
    +              uri: file:/home/configsvc/config-repo

    If {application}/{profile} does not match any of the patterns, it uses the default URI defined under spring.cloud.config.server.git.uri. +In the above example, for the simple repository, the pattern is simple/* (it only matches one application named simple in all profiles). The local repository matches all application names beginning with local in all profiles (the /* suffix is added automatically to any pattern that does not have a profile matcher).

    [Note]Note

    The one-liner short cut used in the simple example can be used only if the only property to be set is the URI. +If you need to set anything else (credentials, pattern, and so on) you need to use the full form.

    The pattern property in the repo is actually an array, so you can use a YAML array (or [0], [1], etc. suffixes in properties files) to bind to multiple patterns. +You may need to do so if you are going to run apps with multiple profiles, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          repos:
    +            development:
    +              pattern:
    +                - '*/development'
    +                - '*/staging'
    +              uri: https://github.com/development/config-repo
    +            staging:
    +              pattern:
    +                - '*/qa'
    +                - '*/production'
    +              uri: https://github.com/staging/config-repo
    [Note]Note

    Spring Cloud guesses that a pattern containing a profile that does not end in * implies that you actually want to match a list of profiles starting with this pattern (so */staging is a shortcut for ["*/staging", "*/staging,*"], and so on). +This is common where, for instance, you need to run applications in the development profile locally but also the cloud profile remotely.

    Every repository can also optionally store config files in sub-directories, and patterns to search for those directories can be specified as searchPaths. +The following example shows a config file at the top level:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          searchPaths: foo,bar*

    In the preceding example, the server searches for config files in the top level and in the foo/ sub-directory and also any sub-directory whose name begins with bar.

    By default, the server clones remote repositories when configuration +is first requested. +The server can be configured to clone the repositories at startup, as shown in the following top-level example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://git/common/config-repo.git
    +          repos:
    +            team-a:
    +                pattern: team-a-*
    +                cloneOnStart: true
    +                uri: https://git/team-a/config-repo.git
    +            team-b:
    +                pattern: team-b-*
    +                cloneOnStart: false
    +                uri: https://git/team-b/config-repo.git
    +            team-c:
    +                pattern: team-c-*
    +                uri: https://git/team-a/config-repo.git

    In the preceding example, the server clones team-a’s config-repo on startup, before it +accepts any requests. +All other repositories are not cloned until configuration from the repository is requested.

    [Note]Note

    Setting a repository to be cloned when the Config Server starts up can help to identify a misconfigured configuration source (such as an invalid repository URI) quickly, while the Config Server is starting up. +With cloneOnStart not enabled for a configuration source, the Config Server may start successfully with a misconfigured or invalid configuration source and not detect an error until an application requests configuration from that configuration source.

    Authentication

    To use HTTP basic authentication on the remote repository, add the username and password properties separately (not in the URL), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          username: trolley
    +          password: strongpassword

    If you do not use HTTPS and user credentials, SSH should also work out of the box when you store keys in the default directories (~/.ssh) and the URI points to an SSH location, such as git@github.com:configuration/cloud-configuration. +It is important that an entry for the Git server be present in the ~/.ssh/known_hosts file and that it is in ssh-rsa format. +Other formats (such as ecdsa-sha2-nistp256) are not supported. +To avoid surprises, you should ensure that only one entry is present in the known_hosts file for the Git server and that it matches the URL you provided to the config server. +If you use a hostname in the URL, you want to have exactly that (not the IP) in the known_hosts file. +The repository is accessed by using JGit, so any documentation you find on that should be applicable. +HTTPS proxy settings can be set in ~/.git/config or (in the same way as for any other JVM process) with +system properties (-Dhttps.proxyHost and -Dhttps.proxyPort).

    [Tip]Tip

    If you do not know where your ~/.git directory is, use git config --global to manipulate the settings (for example, git config --global http.sslVerify false).

    Authentication with AWS CodeCommit

    Spring Cloud Config Server also supports AWS CodeCommit authentication. +AWS CodeCommit uses an authentication helper when using Git from the command line. +This helper is not used with the JGit library, so a JGit CredentialProvider for AWS CodeCommit is created if the Git URI matches the AWS CodeCommit pattern. +AWS CodeCommit URIs follow this pattern://git-codecommit.${AWS_REGION}.amazonaws.com/${repopath}.

    If you provide a username and password with an AWS CodeCommit URI, they must be the AWS accessKeyId and secretAccessKey that provide access to the repository. +If you do not specify a username and password, the accessKeyId and secretAccessKey are retrieved by using the AWS Default Credential Provider Chain.

    If your Git URI matches the CodeCommit URI pattern (shown earlier), you must provide valid AWS credentials in the username and password or in one of the locations supported by the default credential provider chain. +AWS EC2 instances may use IAM Roles for EC2 Instances.

    [Note]Note

    The aws-java-sdk-core jar is an optional dependency. +If the aws-java-sdk-core jar is not on your classpath, the AWS Code Commit credential provider is not created, regardless of the git server URI.

    Git SSH configuration using properties

    By default, the JGit library used by Spring Cloud Config Server uses SSH configuration files such as ~/.ssh/known_hosts and /etc/ssh/ssh_config when connecting to Git repositories by using an SSH URI. +In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. +For those cases, SSH configuration can be set by using Java properties. +In order to activate property-based SSH configuration, the spring.cloud.config.server.git.ignoreLocalSshSettings property must be set to true, as shown in the following example:

      spring:
    +    cloud:
    +      config:
    +        server:
    +          git:
    +            uri: git@gitserver.com:team/repo1.git
    +            ignoreLocalSshSettings: true
    +            hostKey: someHostKey
    +            hostKeyAlgorithm: ssh-rsa
    +            privateKey: |
    +                         -----BEGIN RSA PRIVATE KEY-----
    +                         MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX
    +                         IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF
    +                         ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud
    +                         1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i
    +                         oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W
    +                         DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd
    +                         fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b
    +                         BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG
    +                         EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj
    +                         5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8
    +                         +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe
    +                         pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG
    +                         ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ
    +                         xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW
    +                         dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi
    +                         PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX
    +                         VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z
    +                         FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R
    +                         gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4
    +                         VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV
    +                         cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee
    +                         KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N
    +                         CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs
    +                         q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J
    +                         69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT
    +                         -----END RSA PRIVATE KEY-----

    The following table describes the SSH configuration properties.

    Table 5.1. SSH Configuration Properties

    Property NameRemarks

    ignoreLocalSshSettings

    If true, use property-based instead of file-based SSH config. Must be set at as spring.cloud.config.server.git.ignoreLocalSshSettings, not inside a repository definition.

    privateKey

    Valid SSH private key. Must be set if ignoreLocalSshSettings is true and Git URI is SSH format.

    hostKey

    Valid SSH host key. Must be set if hostKeyAlgorithm is also set.

    hostKeyAlgorithm

    One of ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521. Must be set if hostKey is also set.

    strictHostKeyChecking

    true or false. If false, ignore errors with host key.

    knownHostsFile

    Location of custom .known_hosts file.

    preferredAuthentications

    Override server authentication method order. This should allow for evading login prompts if server has keyboard-interactive authentication before the publickey method.


    Placeholders in Git Search Paths

    Spring Cloud Config Server also supports a search path with placeholders for the {application} and {profile} (and {label} if +you need it), as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          searchPaths: '{application}'

    The preceding listing causes a search of the repository for files in the same name as the directory (as well as the top level). +Wildcards are also valid in a search path with placeholders (any matching directory is included in the search).

    Force pull in Git Repositories

    As mentioned earlier, Spring Cloud Config Server makes a clone of the remote git repository in case the local copy gets dirty (for example, +folder content changes by an OS process) such that Spring Cloud Config Server cannot update the local copy from remote repository.

    To solve this issue, there is a force-pull property that makes Spring Cloud Config Server force pull from the remote repository if the local copy is dirty, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          force-pull: true

    If you have a multiple-repositories configuration, you can configure the force-pull property per repository, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://git/common/config-repo.git
    +          force-pull: true
    +          repos:
    +            team-a:
    +                pattern: team-a-*
    +                uri: https://git/team-a/config-repo.git
    +                force-pull: true
    +            team-b:
    +                pattern: team-b-*
    +                uri: https://git/team-b/config-repo.git
    +                force-pull: true
    +            team-c:
    +                pattern: team-c-*
    +                uri: https://git/team-a/config-repo.git
    [Note]Note

    The default value for force-pull property is false.

    Deleting untracked branches in Git Repositories

    As Spring Cloud Config Server has a clone of the remote git repository +after check-outing branch to local repo (e.g fetching properties by label) it will keep this branch +forever or till the next server restart (which creates new local repo). +So there could be a case when remote branch is deleted but local copy of it is still available for fetching. +And if Spring Cloud Config Server client service starts with --spring.cloud.config.label=deletedRemoteBranch,master +it will fetch properties from deletedRemoteBranch local branch, but not from master.

    In order to keep local repository branches clean and up to remote - deleteUntrackedBranches property could be set. +It will make Spring Cloud Config Server force delete untracked branches from local repository. +Example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          deleteUntrackedBranches: true
    [Note]Note

    The default value for deleteUntrackedBranches property is false.

    Git Refresh Rate

    You can control how often the config server will fetch updated configuration data +from your Git backend by using spring.cloud.config.server.git.refreshRate. The +value of this property is specified in seconds. By default the value is 0, meaning +the config server will fetch updated configuration from the Git repo every time it +is requested.

    5.1.2 Version Control Backend Filesystem Use

    [Warning]Warning

    With VCS-based backends (git, svn), files are checked out or cloned to the local filesystem. +By default, they are put in the system temporary directory with a prefix of config-repo-. +On linux, for example, it could be /tmp/config-repo-<randomid>. +Some operating systems routinely clean out temporary directories. +This can lead to unexpected behavior, such as missing properties. +To avoid this problem, change the directory that Config Server uses by setting spring.cloud.config.server.git.basedir or spring.cloud.config.server.svn.basedir to a directory that does not reside in the system temp structure.

    5.1.3 File System Backend

    There is also a native profile in the Config Server that does not use Git but loads the config files from the local classpath or file system (any static URL you want to point to with spring.cloud.config.server.native.searchLocations). +To use the native profile, launch the Config Server with spring.profiles.active=native.

    [Note]Note

    Remember to use the file: prefix for file resources (the default without a prefix is usually the classpath). +As with any Spring Boot configuration, you can embed ${}-style environment placeholders, but remember that absolute paths in Windows require an extra / (for example, file:///${user.home}/config-repo).

    [Warning]Warning

    The default value of the searchLocations is identical to a local Spring Boot application (that is, [classpath:/, classpath:/config, +file:./, file:./config]). +This does not expose the application.properties from the server to all clients, because any property sources present in the server are removed before being sent to the client.

    [Tip]Tip

    A filesystem backend is great for getting started quickly and for testing. +To use it in production, you need to be sure that the file system is reliable and shared across all instances of the Config Server.

    The search locations can contain placeholders for {application}, {profile}, and {label}. +In this way, you can segregate the directories in the path and choose a strategy that makes sense for you (such as subdirectory per application or subdirectory per profile).

    If you do not use placeholders in the search locations, this repository also appends the {label} parameter of the HTTP resource to a suffix on the search path, so properties files are loaded from each search location and a subdirectory with the same name as the label (the labelled properties take precedence in the Spring Environment). +Thus, the default behaviour with no placeholders is the same as adding a search location ending with /{label}/. +For example, file:/tmp/config is the same as file:/tmp/config,file:/tmp/config/{label}. +This behavior can be disabled by setting spring.cloud.config.server.native.addLabelLocations=false.

    5.1.4 Vault Backend

    Spring Cloud Config Server also supports Vault as a backend.

    For more information on Vault, see the Vault quick start guide.

    To enable the config server to use a Vault backend, you can run your config server with the vault profile. +For example, in your config server’s application.properties, you can add spring.profiles.active=vault.

    By default, the config server assumes that your Vault server runs at http://127.0.0.1:8200. +It also assumes that the name of backend is secret and the key is application. +All of these defaults can be configured in your config server’s application.properties. +The following table describes configurable Vault properties:

    NameDefault Value

    host

    127.0.0.1

    port

    8200

    scheme

    http

    backend

    secret

    defaultKey

    application

    profileSeparator

    ,

    kvVersion

    1

    skipSslValidation

    false

    timeout

    5

    namespace

    null

    [Important]Important

    All of the properties in the preceding table must be prefixed with spring.cloud.config.server.vault or placed in the correct Vault section of a composite configuration.

    All configurable properties can be found in org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.

    Vault 0.10.0 introduced a versioned key-value backend (k/v backend version 2) that exposes a different API than earlier versions, it now requires a data/ between the mount path and the actual context path and wraps secrets in a data object. Setting kvVersion=2 will take this into account.

    Optionally, there is support for the Vault Enterprise X-Vault-Namespace header. To have it sent to Vault set the namespace property.

    With your config server running, you can make HTTP requests to the server to retrieve +values from the Vault backend. +To do so, you need a token for your Vault server.

    First, place some data in you Vault, as shown in the following example:

    $ vault kv put secret/application foo=bar baz=bam
    +$ vault kv put secret/myapp foo=myappsbar

    Second, make an HTTP request to your config server to retrieve the values, as shown in the following example:

    $ curl -X "GET" "http://localhost:8888/myapp/default" -H "X-Config-Token: yourtoken"

    You should see a response similar to the following:

    {
    +   "name":"myapp",
    +   "profiles":[
    +      "default"
    +   ],
    +   "label":null,
    +   "version":null,
    +   "state":null,
    +   "propertySources":[
    +      {
    +         "name":"vault:myapp",
    +         "source":{
    +            "foo":"myappsbar"
    +         }
    +      },
    +      {
    +         "name":"vault:application",
    +         "source":{
    +            "baz":"bam",
    +            "foo":"bar"
    +         }
    +      }
    +   ]
    +}

    Multiple Properties Sources

    When using Vault, you can provide your applications with multiple properties sources. +For example, assume you have written data to the following paths in Vault:

    secret/myApp,dev
    +secret/myApp
    +secret/application,dev
    +secret/application

    Properties written to secret/application are available to all applications using the Config Server. +An application with the name, myApp, would have any properties written to secret/myApp and secret/application available to it. +When myApp has the dev profile enabled, properties written to all of the above paths would be available to it, with properties in the first path in the list taking priority over the others.

    5.1.5 Accessing Backends Through a Proxy

    The configuration server can access a Git or Vault backend through an HTTP or HTTPS proxy. This behavior is controlled for either Git or Vault by settings under proxy.http and proxy.https. These settings are per repository, so if you are using a composite environment repository you must configure proxy settings for each backend in the composite individually. If using a network which requires separate proxy servers for HTTP and HTTPS URLs, you can configure both the HTTP and the HTTPS proxy settings for a single backend.

    The following table describes the proxy configuration properties for both HTTP and HTTPS proxies. All of these properties must be prefixed by proxy.http or proxy.https.

    Table 5.2. Proxy Configuration Properties

    Property NameRemarks

    host

    The host of the proxy.

    port

    The port with which to access the proxy.

    nonProxyHosts

    Any hosts which the configuration server should access outside the proxy. If values are provided for both proxy.http.nonProxyHosts and proxy.https.nonProxyHosts, the proxy.http value will be used.

    username

    The username with which to authenticate to the proxy. If values are provided for both proxy.http.username and proxy.https.username, the proxy.http value will be used.

    password

    The password with which to authenticate to the proxy. If values are provided for both proxy.http.password and proxy.https.password, the proxy.http value will be used.


    The following configuration uses an HTTPS proxy to access a Git repository.

    spring:
    +  profiles:
    +    active: git
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: https://github.com/spring-cloud-samples/config-repo
    +          proxy:
    +            https:
    +              host: my-proxy.host.io
    +              password: myproxypassword
    +              port: '3128'
    +              username: myproxyusername
    +              nonProxyHosts: example.com

    5.1.6 Sharing Configuration With All Applications

    Sharing configuration between all applications varies according to which approach you take, as described in the following topics:

    File Based Repositories

    With file-based (git, svn, and native) repositories, resources with file names in application* (application.properties, application.yml, application-*.properties, and so on) are shared between all client applications. +You can use resources with these file names to configure global defaults and have them be overridden by application-specific files as necessary.

    The #_property_overrides[property overrides] feature can also be used for setting global defaults, with placeholders applications +allowed to override them locally.

    [Tip]Tip

    With the native profile (a local file system backend) , you should use an explicit search location that is not part of the server’s own configuration. +Otherwise, the application* resources in the default search locations get removed because they are part of the server.

    Vault Server

    When using Vault as a backend, you can share configuration with all applications by placing configuration in secret/application. +For example, if you run the following Vault command, all applications using the config server will have the properties foo and baz available to them:

    $ vault write secret/application foo=bar baz=bam

    CredHub Server

    When using CredHub as a backend, you can share configuration with all applications by placing configuration in /application/ or by placing it in the default profile for the application. +For example, if you run the following CredHub command, all applications using the config server will have the properties shared.color1 and shared.color2 available to them:

    credhub set --name "/application/profile/master/shared" --type=json
    +value: {"shared.color1": "blue", "shared.color": "red"}
    credhub set --name "/my-app/default/master/more-shared" --type=json
    +value: {"shared.word1": "hello", "shared.word2": "world"}

    5.1.7 JDBC Backend

    Spring Cloud Config Server supports JDBC (relational database) as a backend for configuration properties. +You can enable this feature by adding spring-jdbc to the classpath and using the jdbc profile or by adding a bean of type JdbcEnvironmentRepository. +If you include the right dependencies on the classpath (see the user guide for more details on that), Spring Boot configures a data source.

    The database needs to have a table called PROPERTIES with columns called APPLICATION, PROFILE, and LABEL (with the usual Environment meaning), plus KEY and VALUE for the key and value pairs in Properties style. +All fields are of type String in Java, so you can make them VARCHAR of whatever length you need. +Property values behave in the same way as they would if they came from Spring Boot properties files named {application}-{profile}.properties, including all the encryption and decryption, which will be applied as post-processing steps (that is, not in the repository implementation directly).

    5.1.8 CredHub Backend

    Spring Cloud Config Server supports CredHub as a backend for configuration properties. +You can enable this feature by adding a dependency to Spring CredHub.

    pom.xml.  +

    <dependencies>
    +	<dependency>
    +		<groupId>org.springframework.credhub</groupId>
    +		<artifactId>spring-credhub-starter</artifactId>
    +	</dependency>
    +</dependencies>

    +

    The following configuration uses mutual TLS to access a CredHub:

    spring:
    +  profiles:
    +    active: credhub
    +  cloud:
    +    config:
    +      server:
    +        credhub:
    +          url: https://credhub:8844

    The properties should be stored as JSON, such as:

    credhub set --name "/demo-app/default/master/toggles" --type=json
    +value: {"toggle.button": "blue", "toggle.link": "red"}
    credhub set --name "/demo-app/default/master/abs" --type=json
    +value: {"marketing.enabled": true, "external.enabled": false}

    All client applications with the name spring.cloud.config.name=demo-app will have the following properties available to them:

    {
    +    toggle.button: "blue",
    +    toggle.link: "red",
    +    marketing.enabled: true,
    +    external.enabled: false
    +}
    [Note]Note

    When no profile is specified default will be used and when no label is specified master will be used as a default value. +NOTE: Values added to application will be shared by all the applications.

    OAuth 2.0

    You can authenticate with OAuth 2.0 using UAA as a provider.

    pom.xml.  +

    <dependencies>
    +	<dependency>
    +		<groupId>org.springframework.security</groupId>
    +		<artifactId>spring-security-config</artifactId>
    +	</dependency>
    +	<dependency>
    +		<groupId>org.springframework.security</groupId>
    +		<artifactId>spring-security-oauth2-client</artifactId>
    +	</dependency>
    +</dependencies>

    +

    The following configuration uses OAuth 2.0 and UAA to access a CredHub:

    spring:
    +  profiles:
    +    active: credhub
    +  cloud:
    +    config:
    +      server:
    +        credhub:
    +          url: https://credhub:8844
    +          oauth2:
    +            registration-id: credhub-client
    +  security:
    +    oauth2:
    +      client:
    +        registration:
    +          credhub-client:
    +            provider: uaa
    +            client-id: credhub_config_server
    +            client-secret: asecret
    +            authorization-grant-type: client_credentials
    +        provider:
    +          uaa:
    +            token-uri: https://uaa:8443/oauth/token
    [Note]Note

    The used UAA client-id should have credhub.read as scope.

    5.1.9 Composite Environment Repositories

    In some scenarios, you may wish to pull configuration data from multiple environment repositories. +To do so, you can enable the composite profile in your configuration server’s application properties or YAML file. +If, for example, you want to pull configuration data from a Subversion repository as well as two Git repositories, you can set the following properties for your configuration server:

    spring:
    +  profiles:
    +    active: composite
    +  cloud:
    +    config:
    +      server:
    +        composite:
    +        -
    +          type: svn
    +          uri: file:///path/to/svn/repo
    +        -
    +          type: git
    +          uri: file:///path/to/rex/git/repo
    +        -
    +          type: git
    +          uri: file:///path/to/walter/git/repo

    Using this configuration, precedence is determined by the order in which repositories are listed under the composite key. +In the above example, the Subversion repository is listed first, so a value found in the Subversion repository will override values found for the same property in one of the Git repositories. +A value found in the rex Git repository will be used before a value found for the same property in the walter Git repository.

    If you want to pull configuration data only from repositories that are each of distinct types, you can enable the corresponding profiles, rather than the composite profile, in your configuration server’s application properties or YAML file. +If, for example, you want to pull configuration data from a single Git repository and a single HashiCorp Vault server, you can set the following properties for your configuration server:

    spring:
    +  profiles:
    +    active: git, vault
    +  cloud:
    +    config:
    +      server:
    +        git:
    +          uri: file:///path/to/git/repo
    +          order: 2
    +        vault:
    +          host: 127.0.0.1
    +          port: 8200
    +          order: 1

    Using this configuration, precedence can be determined by an order property. +You can use the order property to specify the priority order for all your repositories. +The lower the numerical value of the order property, the higher priority it has. +The priority order of a repository helps resolve any potential conflicts between repositories that contain values for the same properties.

    [Note]Note

    If your composite environment includes a Vault server as in the previous example, you must include a Vault token in every request made to the configuration server. See Vault Backend.

    [Note]Note

    Any type of failure when retrieving values from an environment repository results in a failure for the entire composite environment.

    [Note]Note

    When using a composite environment, it is important that all repositories contain the same labels. +If you have an environment similar to those in the preceding examples and you request configuration data with the master label but the Subversion repository does not contain a branch called master, the entire request fails.

    Custom Composite Environment Repositories

    In addition to using one of the environment repositories from Spring Cloud, you can also provide your own EnvironmentRepository bean to be included as part of a composite environment. +To do so, your bean must implement the EnvironmentRepository interface. +If you want to control the priority of your custom EnvironmentRepository within the composite environment, you should also implement the Ordered interface and override the getOrdered method. +If you do not implement the Ordered interface, your EnvironmentRepository is given the lowest priority.

    5.1.10 Property Overrides

    The Config Server has an overrides feature that lets the operator provide configuration properties to all applications. +The overridden properties cannot be accidentally changed by the application with the normal Spring Boot hooks. +To declare overrides, add a map of name-value pairs to spring.cloud.config.server.overrides, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        overrides:
    +          foo: bar

    The preceding examples causes all applications that are config clients to read foo=bar, independent of their own configuration.

    [Note]Note

    A configuration system cannot force an application to use configuration data in any particular way. +Consequently, overrides are not enforceable. +However, they do provide useful default behavior for Spring Cloud Config clients.

    [Tip]Tip

    Normally, Spring environment placeholders with ${} can be escaped (and resolved on the client) by using backslash (\) to escape the $ or the {. +For example, \${app.foo:bar} resolves to bar, unless the app provides its own app.foo.

    [Note]Note

    In YAML, you do not need to escape the backslash itself. +However, in properties files, you do need to escape the backslash, when you configure the overrides on the server.

    You can change the priority of all overrides in the client to be more like default values, letting applications supply their own values in environment variables or System properties, by setting the spring.cloud.config.overrideNone=true flag (the default is false) in the remote repository.

    5.2 Health Indicator

    Config Server comes with a Health Indicator that checks whether the configured EnvironmentRepository is working. +By default, it asks the EnvironmentRepository for an application named app, the default profile, and the default label provided by the EnvironmentRepository implementation.

    You can configure the Health Indicator to check more applications along with custom profiles and custom labels, as shown in the following example:

    spring:
    +  cloud:
    +    config:
    +      server:
    +        health:
    +          repositories:
    +            myservice:
    +              label: mylabel
    +            myservice-dev:
    +              name: myservice
    +              profiles: development

    You can disable the Health Indicator by setting spring.cloud.config.server.health.enabled=false.

    5.3 Security

    You can secure your Config Server in any way that makes sense to you (from physical network security to OAuth2 bearer tokens), because Spring Security and Spring Boot offer support for many security arrangements.

    To use the default Spring Boot-configured HTTP Basic security, include Spring Security on the classpath (for example, through spring-boot-starter-security). +The default is a username of user and a randomly generated password. A random password is not useful in practice, so we recommend you configure the password (by setting spring.security.user.password) and encrypt it (see below for instructions on how to do that).

    5.4 Encryption and Decryption

    [Important]Important

    To use the encryption and decryption features you need the full-strength JCE installed in your JVM (it is not included by default). +You can download the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files from Oracle and follow the installation instructions (essentially, you need to replace the two policy files in the JRE lib/security directory with the ones that you downloaded).

    If the remote property sources contain encrypted content (values starting with {cipher}), they are decrypted before sending to clients over HTTP. +The main advantage of this setup is that the property values need not be in plain text when they are at rest (for example, in a git repository). +If a value cannot be decrypted, it is removed from the property source and an additional property is added with the same key but prefixed with invalid and a value that means not applicable (usually <n/a>). +This is largely to prevent cipher text being used as a password and accidentally leaking.

    If you set up a remote config repository for config client applications, it might contain an application.yml similar to the following:

    application.yml.  +

    spring:
    +  datasource:
    +    username: dbuser
    +    password: '{cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ'

    +

    Encrypted values in a .properties file must not be wrapped in quotes. Otherwise, the value is not decrypted. The following example shows values that would work:

    application.properties.  +

    spring.datasource.username: dbuser
    +spring.datasource.password: {cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ

    +

    You can safely push this plain text to a shared git repository, and the secret password remains protected.

    The server also exposes /encrypt and /decrypt endpoints (on the assumption that these are secured and only accessed by authorized agents). +If you edit a remote config file, you can use the Config Server to encrypt values by POSTing to the /encrypt endpoint, as shown in the following example:

    $ curl localhost:8888/encrypt -d mysecret
    +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    [Note]Note

    If the value you encrypt has characters in it that need to be URL encoded, you should use the --data-urlencode option to curl to make sure they are encoded properly.

    [Tip]Tip

    Be sure not to include any of the curl command statistics in the encrypted value. +Outputting the value to a file can help avoid this problem.

    The inverse operation is also available through /decrypt (provided the server is +configured with a symmetric key or a full key pair), as shown in the following example:

    $ curl localhost:8888/decrypt -d 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    +mysecret
    [Tip]Tip

    If you testing with curl, then use --data-urlencode (instead of -d) or set an explicit Content-Type: text/plain to make sure curl encodes the data correctly when there are special characters ('+' is particularly tricky).

    Take the encrypted value and add the {cipher} prefix before you put it in the YAML or properties file and before you commit and push it to a remote (potentially insecure) store.

    The /encrypt and /decrypt endpoints also both accept paths in the form of /*/{application}/{profiles}, which can be used to control cryptography on a per-application (name) and per-profile basis when clients call into the main environment resource.

    [Note]Note

    To control the cryptography in this granular way, you must also provide a @Bean of type TextEncryptorLocator that creates a different encryptor per name and profiles. +The one that is provided by default does not do so (all encryptions use the same key).

    The spring command line client (with Spring Cloud CLI extensions +installed) can also be used to encrypt and decrypt, as shown in the following example:

    $ spring encrypt mysecret --key foo
    +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    +$ spring decrypt --key foo 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
    +mysecret

    To use a key in a file (such as an RSA public key for encryption), prepend +the key value with "@" and provide the file path, as shown in the following example:

    $ spring encrypt mysecret --key @${HOME}/.ssh/id_rsa.pub
    +AQAjPgt3eFZQXwt8tsHAVv/QHiY5sI2dRcR+...
    [Note]Note

    The --key argument is mandatory (despite having a -- prefix).

    5.5 Key Management

    The Config Server can use a symmetric (shared) key or an asymmetric one (RSA key pair). +The asymmetric choice is superior in terms of security, but it is often more convenient to use a symmetric key since it is a single property value to configure in the bootstrap.properties.

    To configure a symmetric key, you need to set encrypt.key to a secret String (or use the ENCRYPT_KEY environment variable to keep it out of plain-text configuration files).

    [Note]Note

    You cannot configure an asymmetric key using encrypt.key.

    To configure an asymmetric key use a keystore (e.g. as +created by the keytool utility that comes with the JDK). The +keystore properties are encrypt.keyStore.* with * equal to

    PropertyDescription

    encrypt.keyStore.location

    Contains a Resource location

    encrypt.keyStore.password

    Holds the password that unlocks the keystore

    encrypt.keyStore.alias

    Identifies which key in the store to use

    encrypt.keyStore.type

    The type of KeyStore to create. Defaults to jks.

    The encryption is done with the public key, and a private key is +needed for decryption. +Thus, in principle, you can configure only the public key in the server if you want to only encrypt (and are prepared to decrypt the values yourself locally with the private key). +In practice, you might not want to do decrypt locally, because it spreads the key management process around all the clients, instead of +concentrating it in the server. +On the other hand, it can be a useful option if your config server is relatively insecure and only a handful of clients need the encrypted properties.

    5.6 Creating a Key Store for Testing

    To create a keystore for testing, you can use a command resembling the following:

    $ keytool -genkeypair -alias mytestkey -keyalg RSA \
    +  -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" \
    +  -keypass changeme -keystore server.jks -storepass letmein
    [Note]Note

    When using JDK 11 or above you may get the following warning when using the command above. In this case +you probably want to make sure the keypass and storepass values match.

    Warning:  Different store and key passwords not supported for PKCS12 KeyStores. Ignoring user-specified -keypass value.

    Put the server.jks file in the classpath (for instance) and then, in +your bootstrap.yml, for the Config Server, create the following settings:

    encrypt:
    +  keyStore:
    +    location: classpath:/server.jks
    +    password: letmein
    +    alias: mytestkey
    +    secret: changeme

    5.7 Using Multiple Keys and Key Rotation

    In addition to the {cipher} prefix in encrypted property values, the Config Server looks for zero or more {name:value} prefixes before the start of the (Base64 encoded) cipher text. +The keys are passed to a TextEncryptorLocator, which can do whatever logic it needs to locate a TextEncryptor for the cipher. +If you have configured a keystore (encrypt.keystore.location), the default locator looks for keys with aliases supplied by the key prefix, with a cipher text like resembling the following:

    foo:
    +  bar: `{cipher}{key:testkey}...`

    The locator looks for a key named "testkey". +A secret can also be supplied by using a {secret:…​} value in the prefix. +However, if it is not supplied, the default is to use the keystore password (which is what you get when you build a keystore and do not specify a secret). +If you do supply a secret, you should also encrypt the secret using a custom SecretLocator.

    When the keys are being used only to encrypt a few bytes of configuration data (that is, they are not being used elsewhere), key rotation is hardly ever necessary on cryptographic grounds. +However, you might occasionally need to change the keys (for example, in the event of a security breach). +In that case, all the clients would need to change their source config files (for example, in git) and use a new {key:…​} prefix in all the ciphers. +Note that the clients need to first check that the key alias is available in the Config Server keystore.

    [Tip]Tip

    If you want to let the Config Server handle all encryption as well as decryption, the {name:value} prefixes can also be added as plain text posted to the /encrypt endpoint, .

    5.8 Serving Encrypted Properties

    Sometimes you want the clients to decrypt the configuration locally, instead of doing it in the server. +In that case, if you provide the encrypt.* configuration to locate a key, you can still have /encrypt and /decrypt endpoints, but you need to explicitly switch off the decryption of outgoing properties by placing spring.cloud.config.server.encrypt.enabled=false in bootstrap.[yml|properties]. +If you do not care about the endpoints, it should work if you do not configure either the key or the enabled flag.

    6. Serving Alternative Formats

    The default JSON format from the environment endpoints is perfect for consumption by Spring applications, because it maps directly onto the Environment abstraction. +If you prefer, you can consume the same data as YAML or Java properties by adding a suffix (".yml", ".yaml" or ".properties") to the resource path. +This can be useful for consumption by applications that do not care about the structure of the JSON endpoints or the extra metadata they provide (for example, an application that is not using Spring might benefit from the simplicity of this approach).

    The YAML and properties representations have an additional flag (provided as a boolean query parameter called resolvePlaceholders) to signal that placeholders in the source documents (in the standard Spring ${…​} form) should be resolved in the output before rendering, where possible. +This is a useful feature for consumers that do not know about the Spring placeholder conventions.

    [Note]Note

    There are limitations in using the YAML or properties formats, mainly in relation to the loss of metadata. +For example, the JSON is structured as an ordered list of property sources, with names that correlate with the source. +The YAML and properties forms are coalesced into a single map, even if the origin of the values has multiple sources, and the names of the original source files are lost. +Also, the YAML representation is not necessarily a faithful representation of the YAML source in a backing repository either. It is constructed from a list of flat property sources, and assumptions have to be made about the form of the keys.

    7. Serving Plain Text

    Instead of using the Environment abstraction (or one of the alternative representations of it in YAML or properties format), your applications might need generic plain-text configuration files that are tailored to their environment. +The Config Server provides these through an additional endpoint at /{application}/{profile}/{label}/{path}, where application, profile, and label have the same meaning as the regular environment endpoint, but path is a path to a file name (such as log.xml). +The source files for this endpoint are located in the same way as for the environment endpoints. +The same search path is used for properties and YAML files. +However, instead of aggregating all matching resources, only the first one to match is returned.

    After a resource is located, placeholders in the normal format (${…​}) are resolved by using the effective Environment for the supplied application name, profile, and label. +In this way, the resource endpoint is tightly integrated with the environment endpoints. +Consider the following example for a GIT or SVN repository:

    application.yml
    +nginx.conf

    where nginx.conf looks like this:

    server {
    +    listen              80;
    +    server_name         ${nginx.server.name};
    +}

    and application.yml like this:

    nginx:
    +  server:
    +    name: example.com
    +---
    +spring:
    +  profiles: development
    +nginx:
    +  server:
    +    name: develop.com

    The /foo/default/master/nginx.conf resource might be as follows:

    server {
    +    listen              80;
    +    server_name         example.com;
    +}

    and /foo/development/master/nginx.conf like this:

    server {
    +    listen              80;
    +    server_name         develop.com;
    +}
    [Note]Note

    As with the source files for environment configuration, the profile is used to resolve the file name. +So, if you want a profile-specific file, /*/development/*/logback.xml can be resolved by a file called logback-development.xml (in preference to logback.xml).

    [Note]Note

    If you do not want to supply the label and let the server use the default label, you can supply a useDefaultLabel request parameter. +So, the preceding example for the default profile could be /foo/default/nginx.conf?useDefaultLabel.

    8. Embedding the Config Server

    The Config Server runs best as a standalone application. +However, if need be, you can embed it in another application. +To do so, use the @EnableConfigServer annotation. +An optional property named spring.cloud.config.server.bootstrap can be useful in this case. +It is a flag to indicate whether the server should configure itself from its own remote repository. +By default, the flag is off, because it can delay startup. +However, when embedded in another application, it makes sense to initialize the same way as any other application. +When setting spring.cloud.config.server.bootstrap to true you must also use a composite environment repository configuration. +For example

    spring:
    +  application:
    +    name: configserver
    +  profiles:
    +    active: composite
    +  cloud:
    +    config:
    +      server:
    +        composite:
    +          - type: native
    +            search-locations: ${HOME}/Desktop/config
    +        bootstrap: true
    [Note]Note

    If you use the bootstrap flag, the config server needs to have its name and repository URI configured in bootstrap.yml.

    To change the location of the server endpoints, you can (optionally) set spring.cloud.config.server.prefix (for example, /config), to serve the resources under a prefix. +The prefix should start but not end with a /. +It is applied to the @RequestMappings in the Config Server (that is, underneath the Spring Boot server.servletPath and server.contextPath prefixes).

    If you want to read the configuration for an application directly from the backend repository (instead of from the config server), you +basically want an embedded config server with no endpoints. +You can switch off the endpoints entirely by not using the @EnableConfigServer annotation (set spring.cloud.config.server.bootstrap=true).

    9. Push Notifications and Spring Cloud Bus

    Many source code repository providers (such as Github, Gitlab, Gitea, Gitee, Gogs, or Bitbucket) notify you of changes in a repository through a webhook. +You can configure the webhook through the provider’s user interface as a URL and a set of events in which you are interested. +For instance, Github uses a POST to the webhook with a JSON body containing a list of commits and a header (X-Github-Event) set to push. +If you add a dependency on the spring-cloud-config-monitor library and activate the Spring Cloud Bus in your Config Server, then a /monitor endpoint is enabled.

    When the webhook is activated, the Config Server sends a RefreshRemoteApplicationEvent targeted at the applications it thinks might have changed. +The change detection can be strategized. +However, by default, it looks for changes in files that match the application name (for example, foo.properties is targeted at the foo application, while application.properties is targeted at all applications). +The strategy to use when you want to override the behavior is PropertyPathNotificationExtractor, which accepts the request headers and body as parameters and returns a list of file paths that changed.

    The default configuration works out of the box with Github, Gitlab, Gitea, Gitee, Gogs or Bitbucket. +In addition to the JSON notifications from Github, Gitlab, Gitee, or Bitbucket, you can trigger a change notification by POSTing to /monitor with form-encoded body parameters in the pattern of path={application}. +Doing so broadcasts to applications matching the {application} pattern (which can contain wildcards).

    [Note]Note

    The RefreshRemoteApplicationEvent is transmitted only if the spring-cloud-bus is activated in both the Config Server and in the client application.

    [Note]Note

    The default configuration also detects filesystem changes in local git repositories. In that case, the webhook is not used. However, as soon as you edit a config file, a refresh is broadcast.

    10. Spring Cloud Config Client

    A Spring Boot application can take immediate advantage of the Spring Config Server (or other external property sources provided by the application developer). +It also picks up some additional useful features related to Environment change events.

    10.1 Config First Bootstrap

    The default behavior for any application that has the Spring Cloud Config Client on the classpath is as follows: +When a config client starts, it binds to the Config Server (through the spring.cloud.config.uri bootstrap configuration property) and initializes Spring Environment with remote property sources.

    The net result of this behavior is that all client applications that want to consume the Config Server need a bootstrap.yml (or an environment variable) with the server address set in spring.cloud.config.uri (it defaults to "http://localhost:8888").

    10.2 Discovery First Bootstrap

    If you use a DiscoveryClient implementation, such as Spring Cloud Netflix and Eureka Service Discovery or Spring Cloud Consul, you can have the Config Server register with the Discovery Service. +However, in the default Config First mode, clients cannot take advantage of the registration.

    If you prefer to use DiscoveryClient to locate the Config Server, you can do so by setting spring.cloud.config.discovery.enabled=true (the default is false). +The net result of doing so is that client applications all need a bootstrap.yml (or an environment variable) with the appropriate discovery configuration. +For example, with Spring Cloud Netflix, you need to define the Eureka server address (for example, in eureka.client.serviceUrl.defaultZone). +The price for using this option is an extra network round trip on startup, to locate the service registration. +The benefit is that, as long as the Discovery Service is a fixed point, the Config Server can change its coordinates. +The default service ID is configserver, but you can change that on the client by setting spring.cloud.config.discovery.serviceId (and on the server, in the usual way for a service, such as by setting spring.application.name).

    The discovery client implementations all support some kind of metadata map (for example, we have eureka.instance.metadataMap for Eureka). +Some additional properties of the Config Server may need to be configured in its service registration metadata so that clients can connect correctly. +If the Config Server is secured with HTTP Basic, you can configure the credentials as user and password. +Also, if the Config Server has a context path, you can set configPath. +For example, the following YAML file is for a Config Server that is a Eureka client:

    bootstrap.yml.  +

    eureka:
    +  instance:
    +    ...
    +    metadataMap:
    +      user: osufhalskjrtl
    +      password: lviuhlszvaorhvlo5847
    +      configPath: /config

    +

    10.3 Config Client Fail Fast

    In some cases, you may want to fail startup of a service if it cannot connect to the Config Server. +If this is the desired behavior, set the bootstrap configuration property spring.cloud.config.fail-fast=true to make the client halt with an Exception.

    10.4 Config Client Retry

    If you expect that the config server may occasionally be unavailable when your application starts, you can make it keep trying after a failure. +First, you need to set spring.cloud.config.fail-fast=true. +Then you need to add spring-retry and spring-boot-starter-aop to your classpath. +The default behavior is to retry six times with an initial backoff interval of 1000ms and an exponential multiplier of 1.1 for subsequent backoffs. +You can configure these properties (and others) by setting the spring.cloud.config.retry.* configuration properties.

    [Tip]Tip

    To take full control of the retry behavior, add a @Bean of type RetryOperationsInterceptor with an ID of configServerRetryInterceptor. +Spring Retry has a RetryInterceptorBuilder that supports creating one.

    10.5 Locating Remote Configuration Resources

    The Config Service serves property sources from /{application}/{profile}/{label}, where the default bindings in the client app are as follows:

    • "name" = ${spring.application.name}
    • "profile" = ${spring.profiles.active} (actually Environment.getActiveProfiles())
    • "label" = "master"
    [Note]Note

    When setting the property ${spring.application.name} do not prefix your app name with the reserved word application- to prevent issues resolving the correct property source.

    You can override all of them by setting spring.cloud.config.* (where * is name, profile or label). +The label is useful for rolling back to previous versions of configuration. +With the default Config Server implementation, it can be a git label, branch name, or commit ID. +Label can also be provided as a comma-separated list. +In that case, the items in the list are tried one by one until one succeeds. +This behavior can be useful when working on a feature branch. +For instance, you might want to align the config label with your branch but make it optional (in that case, use spring.cloud.config.label=myfeature,develop).

    10.6 Specifying Multiple Urls for the Config Server

    To ensure high availability when you have multiple instances of Config Server deployed and expect one or more instances to be unavailable from time to time, you can either specify multiple URLs (as a comma-separated list under the spring.cloud.config.uri property) or have all your instances register in a Service Registry like Eureka ( if using Discovery-First Bootstrap mode ). Note that doing so ensures high availability only when the Config Server is not running (that is, when the application has exited) or when a connection timeout has occurred. For example, if the Config Server returns a 500 (Internal Server Error) response or the Config Client receives a 401 from the Config Server (due to bad credentials or other causes), the Config Client does not try to fetch properties from other URLs. An error of that kind indicates a user issue rather than an availability problem.

    If you use HTTP basic security on your Config Server, it is currently possible to support per-Config Server auth credentials only if you embed the credentials in each URL you specify under the spring.cloud.config.uri property. If you use any other kind of security mechanism, you cannot (currently) support per-Config Server authentication and authorization.

    10.7 Configuring Timeouts

    If you want to configure timeout thresholds:

    • Read timeouts can be configured by using the property spring.cloud.config.request-read-timeout.
    • Connection timeouts can be configured by using the property spring.cloud.config.request-connect-timeout.

    10.8 Security

    If you use HTTP Basic security on the server, clients need to know the password (and username if it is not the default). +You can specify the username and password through the config server URI or via separate username and password properties, as shown in the following example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +     uri: https://user:secret@myconfig.mycompany.com

    +

    The following example shows an alternate way to pass the same information:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +     uri: https://myconfig.mycompany.com
    +     username: user
    +     password: secret

    +

    The spring.cloud.config.password and spring.cloud.config.username values override anything that is provided in the URI.

    If you deploy your apps on Cloud Foundry, the best way to provide the password is through service credentials (such as in the URI, since it does not need to be in a config file). +The following example works locally and for a user-provided service on Cloud Foundry named configserver:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +     uri: ${vcap.services.configserver.credentials.uri:http://user:password@localhost:8888}

    +

    If you use another form of security, you might need to provide a RestTemplate to the ConfigServicePropertySourceLocator (for example, by grabbing it in the bootstrap context and injecting it).

    10.8.1 Health Indicator

    The Config Client supplies a Spring Boot Health Indicator that attempts to load configuration from the Config Server. +The health indicator can be disabled by setting health.config.enabled=false. +The response is also cached for performance reasons. +The default cache time to live is 5 minutes. +To change that value, set the health.config.time-to-live property (in milliseconds).

    10.8.2 Providing A Custom RestTemplate

    In some cases, you might need to customize the requests made to the config server from the client. +Typically, doing so involves passing special Authorization headers to authenticate requests to the server. +To provide a custom RestTemplate:

    1. Create a new configuration bean with an implementation of PropertySourceLocator, as shown in the following example:

    CustomConfigServiceBootstrapConfiguration.java.  +

    @Configuration
    +public class CustomConfigServiceBootstrapConfiguration {
    +    @Bean
    +    public ConfigServicePropertySourceLocator configServicePropertySourceLocator() {
    +        ConfigClientProperties clientProperties = configClientProperties();
    +       ConfigServicePropertySourceLocator configServicePropertySourceLocator =  new ConfigServicePropertySourceLocator(clientProperties);
    +        configServicePropertySourceLocator.setRestTemplate(customRestTemplate(clientProperties));
    +        return configServicePropertySourceLocator;
    +    }
    +}

    +

    1. In resources/META-INF, create a file called +spring.factories and specify your custom configuration, as shown in the following example:

    spring.factories.  +

    org.springframework.cloud.bootstrap.BootstrapConfiguration = com.my.config.client.CustomConfigServiceBootstrapConfiguration

    +

    10.8.3 Vault

    When using Vault as a backend to your config server, the client needs to supply a token for the server to retrieve values from Vault. +This token can be provided within the client by setting spring.cloud.config.token +in bootstrap.yml, as shown in the following example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    config:
    +      token: YourVaultToken

    +

    10.9 Nested Keys In Vault

    Vault supports the ability to nest keys in a value stored in Vault, as shown in the following example:

    echo -n '{"appA": {"secret": "appAsecret"}, "bar": "baz"}' | vault write secret/myapp -

    This command writes a JSON object to your Vault. +To access these values in Spring, you would use the traditional dot(.) annotation, as shown in the following example

    @Value("${appA.secret}")
    +String name = "World";

    The preceding code would sets the value of the name variable to appAsecret.

    Part III. Spring Cloud Netflix

    Greenwich.SR5

    This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms. With a few +simple annotations you can quickly enable and configure the common patterns inside your +application and build large distributed systems with battle-tested Netflix components. The +patterns provided include Service Discovery (Eureka), Circuit Breaker (Hystrix), +Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon).

    11. Service Discovery: Eureka Clients

    Service Discovery is one of the key tenets of a microservice-based architecture. +Trying to hand-configure each client or some form of convention can be difficult to do and can be brittle. +Eureka is the Netflix Service Discovery Server and Client. +The server can be configured and deployed to be highly available, with each server replicating state about the registered services to the others.

    11.1 How to Include Eureka Client

    To include the Eureka Client in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-eureka-client. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    11.2 Registering with Eureka

    When a client registers with Eureka, it provides meta-data about itself — such as host, port, health indicator URL, home page, and other details. +Eureka receives heartbeat messages from each instance belonging to a service. +If the heartbeat fails over a configurable timetable, the instance is normally removed from the registry.

    The following example shows a minimal Eureka client application:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello world";
    +    }
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}

    Note that the preceding example shows a normal Spring Boot application. +By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server. Configuration is required to locate the Eureka server, as shown in the following example:

    application.yml.  +

    eureka:
    +  client:
    +    serviceUrl:
    +      defaultZone: http://localhost:8761/eureka/

    +

    In the preceding example, defaultZone is a magic string fallback value that provides the service URL for any client that does not express a preference (in other words, it is a useful default).

    [Warning]Warning

    The defaultZone property is case sensitive and requires camel case because the serviceUrl property is a Map<String, String>. Therefore, the defaultZone property does not follow the normal Spring Boot snake-case convention of default-zone.

    The default application name (that is, the service ID), virtual host, and non-secure port (taken from the Environment) are ${spring.application.name}, ${spring.application.name} and ${server.port}, respectively.

    Having spring-cloud-starter-netflix-eureka-client on the classpath makes the app into both a Eureka instance (that is, it registers itself) and a client (it can query the registry to locate other services). +The instance behaviour is driven by eureka.instance.* configuration keys, but the defaults are fine if you ensure that your application has a value for spring.application.name (this is the default for the Eureka service ID or VIP).

    See EurekaInstanceConfigBean and EurekaClientConfigBean for more details on the configurable options.

    To disable the Eureka Discovery Client, you can set eureka.client.enabled to false. Eureka Discovery Client will also be disabled when spring.cloud.discovery.enabled is set to false.

    11.3 Authenticating with the Eureka Server

    HTTP basic authentication is automatically added to your eureka client if one of the eureka.client.serviceUrl.defaultZone URLs has credentials embedded in it (curl style, as follows: http://user:password@localhost:8761/eureka). +For more complex needs, you can create a @Bean of type DiscoveryClientOptionalArgs and inject ClientFilter instances into it, all of which is applied to the calls from the client to the server.

    [Note]Note

    Because of a limitation in Eureka, it is not possible to support per-server basic auth credentials, so only the first set that are found is used.

    11.4 Status Page and Health Indicator

    The status page and health indicators for a Eureka instance default to /info and /health respectively, which are the default locations of useful endpoints in a Spring Boot Actuator application. +You need to change these, even for an Actuator application if you use a non-default context path or servlet path (such as server.servletPath=/custom). The following example shows the default values for the two settings:

    application.yml.  +

    eureka:
    +  instance:
    +    statusPageUrlPath: ${server.servletPath}/info
    +    healthCheckUrlPath: ${server.servletPath}/health

    +

    These links show up in the metadata that is consumed by clients and are used in some scenarios to decide whether to send requests to your application, so it is helpful if they are accurate.

    [Note]Note

    In Dalston it was also required to set the status and health check URLs when changing +that management context path. This requirement was removed beginning in Edgware.

    11.5 Registering a Secure Application

    If your app wants to be contacted over HTTPS, you can set two flags in the EurekaInstanceConfig:

    • eureka.instance.[nonSecurePortEnabled]=[false]
    • eureka.instance.[securePortEnabled]=[true]

    Doing so makes Eureka publish instance information that shows an explicit preference for secure communication. +The Spring Cloud DiscoveryClient always returns a URI starting with https for a service configured this way. +Similarly, when a service is configured this way, the Eureka (native) instance information has a secure health check URL.

    Because of the way Eureka works internally, it still publishes a non-secure URL for the status and home pages unless you also override those explicitly. +You can use placeholders to configure the eureka instance URLs, as shown in the following example:

    application.yml.  +

    eureka:
    +  instance:
    +    statusPageUrl: https://${eureka.hostname}/info
    +    healthCheckUrl: https://${eureka.hostname}/health
    +    homePageUrl: https://${eureka.hostname}/

    +

    (Note that ${eureka.hostname} is a native placeholder only available +in later versions of Eureka. You could achieve the same thing with +Spring placeholders as well — for example, by using ${eureka.instance.hostName}.)

    [Note]Note

    If your application runs behind a proxy, and the SSL termination is in the proxy (for example, if you run in Cloud Foundry or other platforms as a service), then you need to ensure that the proxy forwarded headers are intercepted and handled by the application. +If the Tomcat container embedded in a Spring Boot application has explicit configuration for the 'X-Forwarded-\*` headers, this happens automatically. +The links rendered by your app to itself being wrong (the wrong host, port, or protocol) is a sign that you got this configuration wrong.

    11.6 Eureka’s Health Checks

    By default, Eureka uses the client heartbeat to determine if a client is up. +Unless specified otherwise, the Discovery Client does not propagate the current health check status of the application, per the Spring Boot Actuator. +Consequently, after successful registration, Eureka always announces that the application is in 'UP' state. This behavior can be altered by enabling Eureka health checks, which results in propagating application status to Eureka. +As a consequence, every other application does not send traffic to applications in states other then 'UP'. +The following example shows how to enable health checks for the client:

    application.yml.  +

    eureka:
    +  client:
    +    healthcheck:
    +      enabled: true

    +

    [Warning]Warning

    eureka.client.healthcheck.enabled=true should only be set in application.yml. Setting the value in bootstrap.yml causes undesirable side effects, such as registering in Eureka with an UNKNOWN status.

    If you require more control over the health checks, consider implementing your own com.netflix.appinfo.HealthCheckHandler.

    11.7 Eureka Metadata for Instances and Clients

    It is worth spending a bit of time understanding how the Eureka metadata works, so you can use it in a way that makes sense in your platform. +There is standard metadata for information such as hostname, IP address, port numbers, the status page, and health check. +These are published in the service registry and used by clients to contact the services in a straightforward way. +Additional metadata can be added to the instance registration in the eureka.instance.metadataMap, and this metadata is accessible in the remote clients. +In general, additional metadata does not change the behavior of the client, unless the client is made aware of the meaning of the metadata. +There are a couple of special cases, described later in this document, where Spring Cloud already assigns meaning to the metadata map.

    11.7.1 Using Eureka on Cloud Foundry

    Cloud Foundry has a global router so that all instances of the same app have the same hostname (other PaaS solutions with a similar architecture have the same arrangement). +This is not necessarily a barrier to using Eureka. +However, if you use the router (recommended or even mandatory, depending on the way your platform was set up), you need to explicitly set the hostname and port numbers (secure or non-secure) so that they use the router. +You might also want to use instance metadata so that you can distinguish between the instances on the client (for example, in a custom load balancer). +By default, the eureka.instance.instanceId is vcap.application.instance_id, as shown in the following example:

    application.yml.  +

    eureka:
    +  instance:
    +    hostname: ${vcap.application.uris[0]}
    +    nonSecurePort: 80

    +

    Depending on the way the security rules are set up in your Cloud Foundry instance, you might be able to register and use the IP address of the host VM for direct service-to-service calls. +This feature is not yet available on Pivotal Web Services (PWS).

    11.7.2 Using Eureka on AWS

    If the application is planned to be deployed to an AWS cloud, the Eureka instance must be configured to be AWS-aware. You can do so by customizing the EurekaInstanceConfigBean as follows:

    @Bean
    +@Profile("!default")
    +public EurekaInstanceConfigBean eurekaInstanceConfig(InetUtils inetUtils) {
    +  EurekaInstanceConfigBean b = new EurekaInstanceConfigBean(inetUtils);
    +  AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka");
    +  b.setDataCenterInfo(info);
    +  return b;
    +}

    11.7.3 Changing the Eureka Instance ID

    A vanilla Netflix Eureka instance is registered with an ID that is equal to its host name (that is, there is only one service per host). +Spring Cloud Eureka provides a sensible default, which is defined as follows:

    ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}}

    An example is myhost:myappname:8080.

    By using Spring Cloud, you can override this value by providing a unique identifier in eureka.instance.instanceId, as shown in the following example:

    application.yml.  +

    eureka:
    +  instance:
    +    instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}

    +

    With the metadata shown in the preceding example and multiple service instances deployed on localhost, the random value is inserted there to make the instance unique. +In Cloud Foundry, the vcap.application.instance_id is populated automatically in a Spring Boot application, so the random value is not needed.

    11.8 Using the EurekaClient

    Once you have an application that is a discovery client, you can use it to discover service instances from the Eureka Server. +One way to do so is to use the native com.netflix.discovery.EurekaClient (as opposed to the Spring Cloud DiscoveryClient), as shown in the following example:

    @Autowired
    +private EurekaClient discoveryClient;
    +
    +public String serviceUrl() {
    +    InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false);
    +    return instance.getHomePageUrl();
    +}
    [Tip]Tip

    Do not use the EurekaClient in a @PostConstruct method or in a @Scheduled method (or anywhere where the ApplicationContext might not be started yet). +It is initialized in a SmartLifecycle (with phase=0), so the earliest you can rely on it being available is in another SmartLifecycle with a higher phase.

    11.8.1 EurekaClient without Jersey

    By default, EurekaClient uses Jersey for HTTP communication. +If you wish to avoid dependencies from Jersey, you can exclude it from your dependencies. +Spring Cloud auto-configures a transport client based on Spring RestTemplate. +The following example shows Jersey being excluded:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>com.sun.jersey</groupId>
    +            <artifactId>jersey-client</artifactId>
    +        </exclusion>
    +        <exclusion>
    +            <groupId>com.sun.jersey</groupId>
    +            <artifactId>jersey-core</artifactId>
    +        </exclusion>
    +        <exclusion>
    +            <groupId>com.sun.jersey.contribs</groupId>
    +            <artifactId>jersey-apache-client4</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>

    11.9 Alternatives to the Native Netflix EurekaClient

    You need not use the raw Netflix EurekaClient. +Also, it is usually more convenient to use it behind a wrapper of some sort. +Spring Cloud has support for Feign (a REST client builder) and Spring RestTemplate through the logical Eureka service identifiers (VIPs) instead of physical URLs. +To configure Ribbon with a fixed list of physical servers, you can set <client>.ribbon.listOfServers to a comma-separated list of physical addresses (or hostnames), where <client> is the ID of the client.

    You can also use the org.springframework.cloud.client.discovery.DiscoveryClient, which provides a simple API (not specific to Netflix) for discovery clients, as shown in the following example:

    @Autowired
    +private DiscoveryClient discoveryClient;
    +
    +public String serviceUrl() {
    +    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    +    if (list != null && list.size() > 0 ) {
    +        return list.get(0).getUri();
    +    }
    +    return null;
    +}

    11.10 Why Is It so Slow to Register a Service?

    Being an instance also involves a periodic heartbeat to the registry +(through the client’s serviceUrl) with a default duration of 30 seconds. +A service is not available for discovery by clients until the instance, the server, and the client all have the same metadata in their local +cache (so it could take 3 heartbeats). +You can change the period by setting eureka.instance.leaseRenewalIntervalInSeconds. +Setting it to a value of less than 30 speeds up the process of getting clients connected to other services. +In production, it is probably better to stick with the default, because of internal computations in the server that make assumptions about the lease renewal period.

    11.11 Zones

    If you have deployed Eureka clients to multiple zones, you may prefer that those clients use services within the same zone before trying services in another zone. +To set that up, you need to configure your Eureka clients correctly.

    First, you need to make sure you have Eureka servers deployed to each zone and that +they are peers of each other. +See the section on zones and regions +for more information.

    Next, you need to tell Eureka which zone your service is in. +You can do so by using the metadataMap property. +For example, if service 1 is deployed to both zone 1 and zone 2, you need to set the following Eureka properties in service 1:

    Service 1 in Zone 1

    eureka.instance.metadataMap.zone = zone1
    +eureka.client.preferSameZoneEureka = true

    Service 1 in Zone 2

    eureka.instance.metadataMap.zone = zone2
    +eureka.client.preferSameZoneEureka = true

    11.12 Refreshing Eureka Clients

    By default, the EurekaClient bean is refreshable, meaning the Eureka client properties can be changed and refreshed. +When a refresh occurs clients will be unregistered from the Eureka server and there might be a brief moment of time +where all instance of a given service are not available. One way to eliminate this from happening is to disable +the ability to refresh Eureka clients. To do this set eureka.client.refresh.enable=false.

    12. Service Discovery: Eureka Server

    This section describes how to set up a Eureka server.

    12.1 How to Include Eureka Server

    To include Eureka Server in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-eureka-server. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    [Note]Note

    If your project already uses Thymeleaf as its template engine, the Freemarker templates of the Eureka server may not be loaded correctly. In this case it is necessary to configure the template loader manually:

    application.yml.  +

    spring:
    +  freemarker:
    +    template-loader-path: classpath:/templates/
    +    prefer-file-system-access: false

    +

    12.2 How to Run a Eureka Server

    The following example shows a minimal Eureka server:

    @SpringBootApplication
    +@EnableEurekaServer
    +public class Application {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}

    The server has a home page with a UI and HTTP API endpoints for the normal Eureka functionality under /eureka/*.

    The following links have some Eureka background reading: flux capacitor and google group discussion.

    [Tip]Tip

    Due to Gradle’s dependency resolution rules and the lack of a parent bom feature, depending on spring-cloud-starter-netflix-eureka-server can cause failures on application startup. +To remedy this issue, add the Spring Boot Gradle plugin and import the Spring cloud starter parent bom as follows:

    build.gradle.  +

    buildscript {
    +  dependencies {
    +    classpath("org.springframework.boot:spring-boot-gradle-plugin:{spring-boot-docs-version}")
    +  }
    +}
    +
    +apply plugin: "spring-boot"
    +
    +dependencyManagement {
    +  imports {
    +    mavenBom "org.springframework.cloud:spring-cloud-dependencies:{spring-cloud-version}"
    +  }
    +}

    +

    12.3 High Availability, Zones and Regions

    The Eureka server does not have a back end store, but the service instances in the registry all have to send heartbeats to keep their registrations up to date (so this can be done in memory). +Clients also have an in-memory cache of Eureka registrations (so they do not have to go to the registry for every request to a service).

    By default, every Eureka server is also a Eureka client and requires (at least one) service URL to locate a peer. +If you do not provide it, the service runs and works, but it fills your logs with a lot of noise about not being able to register with the peer.

    See also below for details of Ribbon support on the client side for Zones and Regions.

    12.4 Standalone Mode

    The combination of the two caches (client and server) and the heartbeats make a standalone Eureka server fairly resilient to failure, as long as there is some sort of monitor or elastic runtime (such as Cloud Foundry) keeping it alive. +In standalone mode, you might prefer to switch off the client side behavior so that it does not keep trying and failing to reach its peers. +The following example shows how to switch off the client-side behavior:

    application.yml (Standalone Eureka Server).  +

    server:
    +  port: 8761
    +
    +eureka:
    +  instance:
    +    hostname: localhost
    +  client:
    +    registerWithEureka: false
    +    fetchRegistry: false
    +    serviceUrl:
    +      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

    +

    Notice that the serviceUrl is pointing to the same host as the local instance.

    12.5 Peer Awareness

    Eureka can be made even more resilient and available by running multiple instances and asking them to register with each other. +In fact, this is the default behavior, so all you need to do to make it work is add a valid serviceUrl to a peer, as shown in the following example:

    application.yml (Two Peer Aware Eureka Servers).  +

    ---
    +spring:
    +  profiles: peer1
    +eureka:
    +  instance:
    +    hostname: peer1
    +  client:
    +    serviceUrl:
    +      defaultZone: http://peer2/eureka/
    +
    +---
    +spring:
    +  profiles: peer2
    +eureka:
    +  instance:
    +    hostname: peer2
    +  client:
    +    serviceUrl:
    +      defaultZone: http://peer1/eureka/

    +

    In the preceding example, we have a YAML file that can be used to run the same server on two hosts (peer1 and peer2) by running it in different Spring profiles. +You could use this configuration to test the peer awareness on a single host (there is not much value in doing that in production) by manipulating /etc/hosts to resolve the host names. +In fact, the eureka.instance.hostname is not needed if you are running on a machine that knows its own hostname (by default, it is looked up by using java.net.InetAddress).

    You can add multiple peers to a system, and, as long as they are all connected to each other by at least one edge, they synchronize +the registrations amongst themselves. +If the peers are physically separated (inside a data center or between multiple data centers), then the system can, in principle, survive split-brain type failures. +You can add multiple peers to a system, and as long as they are all +directly connected to each other, they will synchronize +the registrations amongst themselves.

    application.yml (Three Peer Aware Eureka Servers).  +

    eureka:
    +  client:
    +    serviceUrl:
    +      defaultZone: http://peer1/eureka/,http://peer2/eureka/,http://peer3/eureka/
    +
    +---
    +spring:
    +  profiles: peer1
    +eureka:
    +  instance:
    +    hostname: peer1
    +
    +---
    +spring:
    +  profiles: peer2
    +eureka:
    +  instance:
    +    hostname: peer2
    +
    +---
    +spring:
    +  profiles: peer3
    +eureka:
    +  instance:
    +    hostname: peer3

    +

    12.6 When to Prefer IP Address

    In some cases, it is preferable for Eureka to advertise the IP addresses of services rather than the hostname. +Set eureka.instance.preferIpAddress to true and, when the application registers with eureka, it uses its IP address rather than its hostname.

    [Tip]Tip

    If the hostname cannot be determined by Java, then the IP address is sent to Eureka. +Only explict way of setting the hostname is by setting eureka.instance.hostname property. +You can set your hostname at the run-time by using an environment variable — for example, eureka.instance.hostname=${HOST_NAME}.

    12.7 Securing The Eureka Server

    You can secure your Eureka server simply by adding Spring Security to your +server’s classpath via spring-boot-starter-security. By default when Spring Security is on the classpath it will require that +a valid CSRF token be sent with every request to the app. Eureka clients will not generally possess a valid +cross site request forgery (CSRF) token you will need to disable this requirement for the /eureka/** endpoints. +For example:

    @EnableWebSecurity
    +class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    +
    +    @Override
    +    protected void configure(HttpSecurity http) throws Exception {
    +        http.csrf().ignoringAntMatchers("/eureka/**");
    +        super.configure(http);
    +    }
    +}

    For more information on CSRF see the Spring Security documentation.

    A demo Eureka Server can be found in the Spring Cloud Samples repo.

    12.8 JDK 11 Support

    The JAXB modules which the Eureka server depends upon were removed in JDK 11. If you intend to use JDK 11 +when running a Eureka server you must include these dependencies in your POM or Gradle file.

    <dependency>
    +	<groupId>org.glassfish.jaxb</groupId>
    +	<artifactId>jaxb-runtime</artifactId>
    +</dependency>

    13. Circuit Breaker: Hystrix Clients

    Netflix has created a library called Hystrix that implements the circuit breaker pattern. +In a microservice architecture, it is common to have multiple layers of service calls, as shown in the following example:

    Figure 13.1. Microservice Graph

    Hystrix

    A service failure in the lower level of services can cause cascading failure all the way up to the user. +When calls to a particular service exceed circuitBreaker.requestVolumeThreshold (default: 20 requests) and the failure percentage is greater than circuitBreaker.errorThresholdPercentage (default: >50%) in a rolling window defined by metrics.rollingStats.timeInMilliseconds (default: 10 seconds), the circuit opens and the call is not made. +In cases of error and an open circuit, a fallback can be provided by the developer.

    Figure 13.2. Hystrix fallback prevents cascading failures

    HystrixFallback

    Having an open circuit stops cascading failures and allows overwhelmed or failing services time to recover. +The fallback can be another Hystrix protected call, static data, or a sensible empty value. +Fallbacks may be chained so that the first fallback makes some other business call, which in turn falls back to static data.

    13.1 How to Include Hystrix

    To include Hystrix in your project, use the starter with a group ID of org.springframework.cloud +and a artifact ID of spring-cloud-starter-netflix-hystrix. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    The following example shows a minimal Eureka server with a Hystrix circuit breaker:

    @SpringBootApplication
    +@EnableCircuitBreaker
    +public class Application {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}
    +
    +@Component
    +public class StoreIntegration {
    +
    +    @HystrixCommand(fallbackMethod = "defaultStores")
    +    public Object getStores(Map<String, Object> parameters) {
    +        //do stuff that might fail
    +    }
    +
    +    public Object defaultStores(Map<String, Object> parameters) {
    +        return /* something useful */;
    +    }
    +}

    The @HystrixCommand is provided by a Netflix contrib library called javanica. +Spring Cloud automatically wraps Spring beans with that annotation in a proxy that is connected to the Hystrix circuit breaker. +The circuit breaker calculates when to open and close the circuit and what to do in case of a failure.

    To configure the @HystrixCommand you can use the commandProperties +attribute with a list of @HystrixProperty annotations. See +here +for more details. See the Hystrix wiki +for details on the properties available.

    13.2 Propagating the Security Context or Using Spring Scopes

    If you want some thread local context to propagate into a @HystrixCommand, the default declaration does not work, because it executes the command in a thread pool (in case of timeouts). +You can switch Hystrix to use the same thread as the caller through configuration or directly in the annotation, by asking it to use a different Isolation Strategy. +The following example demonstrates setting the thread in the annotation:

    @HystrixCommand(fallbackMethod = "stubMyService",
    +    commandProperties = {
    +      @HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
    +    }
    +)
    +...

    The same thing applies if you are using @SessionScope or @RequestScope. +If you encounter a runtime exception that says it cannot find the scoped context, you need to use the same thread.

    You also have the option to set the hystrix.shareSecurityContext property to true. +Doing so auto-configures a Hystrix concurrency strategy plugin hook to transfer the SecurityContext from your main thread to the one used by the Hystrix command. +Hystrix does not let multiple Hystrix concurrency strategy be registered so an extension mechanism is available by declaring your own HystrixConcurrencyStrategy as a Spring bean. +Spring Cloud looks for your implementation within the Spring context and wrap it inside its own plugin.

    13.3 Health Indicator

    The state of the connected circuit breakers are also exposed in the /health endpoint of the calling application, as shown in the following example:

    {
    +    "hystrix": {
    +        "openCircuitBreakers": [
    +            "StoreIntegration::getStoresByLocationLink"
    +        ],
    +        "status": "CIRCUIT_OPEN"
    +    },
    +    "status": "UP"
    +}

    13.4 Hystrix Metrics Stream

    To enable the Hystrix metrics stream, include a dependency on spring-boot-starter-actuator and set +management.endpoints.web.exposure.include: hystrix.stream. +Doing so exposes the /actuator/hystrix.stream as a management endpoint, as shown in the following example:

        <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-actuator</artifactId>
    +    </dependency>

    14. Circuit Breaker: Hystrix Dashboard

    One of the main benefits of Hystrix is the set of metrics it gathers about each HystrixCommand. +The Hystrix Dashboard displays the health of each circuit breaker in an efficient manner.

    Figure 14.1. Hystrix Dashboard

    Hystrix

    15. Hystrix Timeouts And Ribbon Clients

    When using Hystrix commands that wrap Ribbon clients you want to make sure your Hystrix timeout +is configured to be longer than the configured Ribbon timeout, including any potential +retries that might be made. For example, if your Ribbon connection timeout is one second and +the Ribbon client might retry the request three times, than your Hystrix timeout should +be slightly more than three seconds.

    15.1 How to Include the Hystrix Dashboard

    To include the Hystrix Dashboard in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-hystrix-dashboard. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    To run the Hystrix Dashboard, annotate your Spring Boot main class with @EnableHystrixDashboard. +Then visit /hystrix and point the dashboard to an individual instance’s /hystrix.stream endpoint in a Hystrix client application.

    [Note]Note

    When connecting to a /hystrix.stream endpoint that uses HTTPS, the certificate used by the server must be trusted by the JVM. +If the certificate is not trusted, you must import the certificate into the JVM in order for the Hystrix Dashboard to make a successful connection to the stream endpoint.

    15.2 Turbine

    Looking at an individual instance’s Hystrix data is not very useful in terms of the overall health of the system. Turbine is an application that aggregates all of the relevant /hystrix.stream endpoints into a combined /turbine.stream for use in the Hystrix Dashboard. +Individual instances are located through Eureka. +Running Turbine requires annotating your main class with the @EnableTurbine annotation (for example, by using spring-cloud-starter-netflix-turbine to set up the classpath). +All of the documented configuration properties from the Turbine 1 wiki apply. +The only difference is that the turbine.instanceUrlSuffix does not need the port prepended, as this is handled automatically unless turbine.instanceInsertPort=false.

    [Note]Note

    By default, Turbine looks for the /hystrix.stream endpoint on a registered instance by looking up its hostName and port entries in Eureka and then appending /hystrix.stream to it. +If the instance’s metadata contains management.port, it is used instead of the port value for the /hystrix.stream endpoint. +By default, the metadata entry called management.port is equal to the management.port configuration property. +It can be overridden though with following configuration:

    eureka:
    +  instance:
    +    metadata-map:
    +      management.port: ${management.port:8081}

    The turbine.appConfig configuration key is a list of Eureka serviceIds that turbine uses to lookup instances. +The turbine stream is then used in the Hystrix dashboard with a URL similar to the following:

    https://my.turbine.server:8080/turbine.stream?cluster=CLUSTERNAME

    The cluster parameter can be omitted if the name is default. +The cluster parameter must match an entry in turbine.aggregator.clusterConfig. +Values returned from Eureka are upper-case. Consequently, the following example works if there is an application called customers registered with Eureka:

    turbine:
    +  aggregator:
    +    clusterConfig: CUSTOMERS
    +  appConfig: customers

    If you need to customize which cluster names should be used by Turbine (because you do not want to store cluster names in +turbine.aggregator.clusterConfig configuration), provide a bean of type TurbineClustersProvider.

    The clusterName can be customized by a SPEL expression in turbine.clusterNameExpression with root as an instance of InstanceInfo. +The default value is appName, which means that the Eureka serviceId becomes the cluster key (that is, the InstanceInfo for customers has an appName of CUSTOMERS). +A different example is turbine.clusterNameExpression=aSGName, which gets the cluster name from the AWS ASG name. +The following listing shows another example:

    turbine:
    +  aggregator:
    +    clusterConfig: SYSTEM,USER
    +  appConfig: customers,stores,ui,admin
    +  clusterNameExpression: metadata['cluster']

    In the preceding example, the cluster name from four services is pulled from their metadata map and is expected to have values that include SYSTEM and USER.

    To use the default cluster for all apps, you need a string literal expression (with single quotes and escaped with double quotes if it is in YAML as well):

    turbine:
    +  appConfig: customers,stores
    +  clusterNameExpression: "'default'"

    Spring Cloud provides a spring-cloud-starter-netflix-turbine that has all the dependencies you need to get a Turbine server running. To add Turbine, create a Spring Boot application and annotate it with @EnableTurbine.

    [Note]Note

    By default, Spring Cloud lets Turbine use the host and port to allow multiple processes per host, per cluster. +If you want the native Netflix behavior built into Turbine to not allow multiple processes per host, per cluster (the key to the instance ID is the hostname), set turbine.combineHostPort=false.

    15.2.1 Clusters Endpoint

    In some situations it might be useful for other applications to know what custers have been configured +in Turbine. To support this you can use the /clusters endpoint which will return a JSON array of +all the configured clusters.

    GET /clusters.  +

    [
    +  {
    +    "name": "RACES",
    +    "link": "http://localhost:8383/turbine.stream?cluster=RACES"
    +  },
    +  {
    +    "name": "WEB",
    +    "link": "http://localhost:8383/turbine.stream?cluster=WEB"
    +  }
    +]

    +

    This endpoint can be disabled by setting turbine.endpoints.clusters.enabled to false.

    15.3 Turbine Stream

    In some environments (such as in a PaaS setting), the classic Turbine model of pulling metrics from all the distributed Hystrix commands does not work. +In that case, you might want to have your Hystrix commands push metrics to Turbine. Spring Cloud enables that with messaging. +To do so on the client, add a dependency to spring-cloud-netflix-hystrix-stream and the spring-cloud-starter-stream-* of your choice. +See the Spring Cloud Stream documentation for details on the brokers and how to configure the client credentials. It should work out of the box for a local broker.

    On the server side, create a Spring Boot application and annotate it with @EnableTurbineStream. +The Turbine Stream server requires the use of Spring Webflux, therefore spring-boot-starter-webflux needs to be included in your project. +By default spring-boot-starter-webflux is included when adding spring-cloud-starter-netflix-turbine-stream to your application.

    You can then point the Hystrix Dashboard to the Turbine Stream Server instead of individual Hystrix streams. +If Turbine Stream is running on port 8989 on myhost, then put http://myhost:8989 in the stream input field in the Hystrix Dashboard. +Circuits are prefixed by their respective serviceId, followed by a dot (.), and then the circuit name.

    Spring Cloud provides a spring-cloud-starter-netflix-turbine-stream that has all the dependencies you need to get a Turbine Stream server running. +You can then add the Stream binder of your choice — such as spring-cloud-starter-stream-rabbit.

    Turbine Stream server also supports the cluster parameter. +Unlike Turbine server, Turbine Stream uses eureka serviceIds as cluster names and these are not configurable.

    If Turbine Stream server is running on port 8989 on my.turbine.server and you have two eureka serviceIds customers and products in your environment, the following URLs will be available on your Turbine Stream server. default and empty cluster name will provide all metrics that Turbine Stream server receives.

    https://my.turbine.sever:8989/turbine.stream?cluster=customers
    +https://my.turbine.sever:8989/turbine.stream?cluster=products
    +https://my.turbine.sever:8989/turbine.stream?cluster=default
    +https://my.turbine.sever:8989/turbine.stream

    So, you can use eureka serviceIds as cluster names for your Turbine dashboard (or any compatible dashboard). +You don’t need to configure any properties like turbine.appConfig, turbine.clusterNameExpression and turbine.aggregator.clusterConfig for your Turbine Stream server.

    [Note]Note

    Turbine Stream server gathers all metrics from the configured input channel with Spring Cloud Stream. It means that it doesn’t gather Hystrix metrics actively from each instance. It just can provide metrics that were already gathered into the input channel by each instance.

    16. Client Side Load Balancer: Ribbon

    Ribbon is a client-side load balancer that gives you a lot of control over the behavior of HTTP and TCP clients. +Feign already uses Ribbon, so, if you use @FeignClient, this section also applies.

    A central concept in Ribbon is that of the named client. +Each load balancer is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer (for example, by using the @FeignClient annotation). +On demand, Spring Cloud creates a new ensemble as an ApplicationContext for each named client by using +RibbonClientConfiguration. +This contains (amongst other things) an ILoadBalancer, a RestClient, and a ServerListFilter.

    16.1 How to Include Ribbon

    To include Ribbon in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-ribbon. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    16.2 Customizing the Ribbon Client

    You can configure some bits of a Ribbon client by using external properties in <client>.ribbon.*, which is similar to using the Netflix APIs natively, except that you can use Spring Boot configuration files. +The native options can be inspected as static fields in CommonClientConfigKey (part of ribbon-core).

    Spring Cloud also lets you take full control of the client by declaring additional configuration (on top of the RibbonClientConfiguration) using @RibbonClient, as shown in the following example:

    @Configuration
    +@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
    +public class TestConfiguration {
    +}

    In this case, the client is composed from the components already in RibbonClientConfiguration, together with any in CustomConfiguration (where the latter generally overrides the former).

    [Warning]Warning

    The CustomConfiguration clas must be a @Configuration class, but take care that it is not in a @ComponentScan for the main application context. +Otherwise, it is shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication), you need to take steps to avoid it being included (for instance, you can put it in a separate, non-overlapping package or specify the packages to scan explicitly in the @ComponentScan).

    The following table shows the beans that Spring Cloud Netflix provides by default for Ribbon:

    Bean TypeBean NameClass Name

    IClientConfig

    ribbonClientConfig

    DefaultClientConfigImpl

    IRule

    ribbonRule

    ZoneAvoidanceRule

    IPing

    ribbonPing

    DummyPing

    ServerList<Server>

    ribbonServerList

    ConfigurationBasedServerList

    ServerListFilter<Server>

    ribbonServerListFilter

    ZonePreferenceServerListFilter

    ILoadBalancer

    ribbonLoadBalancer

    ZoneAwareLoadBalancer

    ServerListUpdater

    ribbonServerListUpdater

    PollingServerListUpdater

    Creating a bean of one of those type and placing it in a @RibbonClient configuration (such as FooConfiguration above) lets you override each one of the beans described, as shown in the following example:

    @Configuration
    +protected static class FooConfiguration {
    +
    +	@Bean
    +	public ZonePreferenceServerListFilter serverListFilter() {
    +		ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
    +		filter.setZone("myTestZone");
    +		return filter;
    +	}
    +
    +	@Bean
    +	public IPing ribbonPing() {
    +		return new PingUrl();
    +	}
    +
    +}

    The include statement in the preceding example replaces NoOpPing with PingUrl and provides a custom serverListFilter.

    16.3 Customizing the Default for All Ribbon Clients

    A default configuration can be provided for all Ribbon Clients by using the @RibbonClients annotation and registering a default configuration, as shown in the following example:

    @RibbonClients(defaultConfiguration = DefaultRibbonConfig.class)
    +public class RibbonClientDefaultConfigurationTestsConfig {
    +
    +	public static class BazServiceList extends ConfigurationBasedServerList {
    +
    +		public BazServiceList(IClientConfig config) {
    +			super.initWithNiwsConfig(config);
    +		}
    +
    +	}
    +
    +}
    +
    +@Configuration
    +class DefaultRibbonConfig {
    +
    +	@Bean
    +	public IRule ribbonRule() {
    +		return new BestAvailableRule();
    +	}
    +
    +	@Bean
    +	public IPing ribbonPing() {
    +		return new PingUrl();
    +	}
    +
    +	@Bean
    +	public ServerList<Server> ribbonServerList(IClientConfig config) {
    +		return new RibbonClientDefaultConfigurationTestsConfig.BazServiceList(config);
    +	}
    +
    +	@Bean
    +	public ServerListSubsetFilter serverListFilter() {
    +		ServerListSubsetFilter filter = new ServerListSubsetFilter();
    +		return filter;
    +	}
    +
    +}

    16.4 Customizing the Ribbon Client by Setting Properties

    Starting with version 1.2.0, Spring Cloud Netflix now supports customizing Ribbon clients by setting properties to be compatible with the Ribbon documentation.

    This lets you change behavior at start up time in different environments.

    The following list shows the supported properties>:

    • <clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
    • <clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
    • <clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
    • <clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
    • <clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter
    [Note]Note

    Classes defined in these properties have precedence over beans defined by using @RibbonClient(configuration=MyRibbonConfig.class) and the defaults provided by Spring Cloud Netflix.

    To set the IRule for a service name called users, you could set the following properties:

    application.yml.  +

    users:
    +  ribbon:
    +    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    +    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

    +

    See the Ribbon documentation for implementations provided by Ribbon.

    16.5 Using Ribbon with Eureka

    When Eureka is used in conjunction with Ribbon (that is, both are on the classpath), the ribbonServerList is overridden with an extension of DiscoveryEnabledNIWSServerList, which populates the list of servers from Eureka. +It also replaces the IPing interface with NIWSDiscoveryPing, which delegates to Eureka to determine if a server is up. +The ServerList that is installed by default is a DomainExtractingServerList. Its purpose is to make metadata available to the load balancer without using AWS AMI metadata (which is what Netflix relies on). +By default, the server list is constructed with zone information, as provided in the instance metadata (so, on the remote clients, set eureka.instance.metadataMap.zone). +If that is missing and if the approximateZoneFromHostname flag is set, it can use the domain name from the server hostname as a proxy for the zone. +Once the zone information is available, it can be used in a ServerListFilter. +By default, it is used to locate a server in the same zone as the client, because the default is a ZonePreferenceServerListFilter. +By default, the zone of the client is determined in the same way as the remote instances (that is, through eureka.instance.metadataMap.zone).

    [Note]Note

    The orthodox archaius way to set the client zone is through a configuration property called "@zone". +If it is available, Spring Cloud uses that in preference to all other settings (note that the key must be quoted in YAML configuration).

    [Note]Note

    If there is no other source of zone data, then a guess is made, based on the client configuration (as opposed to the instance configuration). +We take eureka.client.availabilityZones, which is a map from region name to a list of zones, and pull out the first zone for the instance’s own region (that is, the eureka.client.region, which defaults to "us-east-1", for compatibility with native Netflix).

    16.6 Example: How to Use Ribbon Without Eureka

    Eureka is a convenient way to abstract the discovery of remote servers so that you do not have to hard code their URLs in clients. +However, if you prefer not to use Eureka, Ribbon and Feign also work. +Suppose you have declared a @RibbonClient for "stores", and Eureka is not in use (and not even on the classpath). +The Ribbon client defaults to a configured server list. +You can supply the configuration as follows:

    application.yml.  +

    stores:
    +  ribbon:
    +    listOfServers: example.com,google.com

    +

    16.7 Example: Disable Eureka Use in Ribbon

    Setting the ribbon.eureka.enabled property to false explicitly disables the use of Eureka in Ribbon, as shown in the following example:

    application.yml.  +

    ribbon:
    +  eureka:
    +   enabled: false

    +

    16.8 Using the Ribbon API Directly

    You can also use the LoadBalancerClient directly, as shown in the following example:

    public class MyClass {
    +    @Autowired
    +    private LoadBalancerClient loadBalancer;
    +
    +    public void doStuff() {
    +        ServiceInstance instance = loadBalancer.choose("stores");
    +        URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
    +        // ... do something with the URI
    +    }
    +}

    16.9 Caching of Ribbon Configuration

    Each Ribbon named client has a corresponding child application Context that Spring Cloud maintains. +This application context is lazily loaded on the first request to the named client. +This lazy loading behavior can be changed to instead eagerly load these child application contexts at startup, by specifying the names of the Ribbon clients, as shown in the following example:

    application.yml.  +

    ribbon:
    +  eager-load:
    +    enabled: true
    +    clients: client1, client2, client3

    +

    16.10 How to Configure Hystrix Thread Pools

    If you change zuul.ribbonIsolationStrategy to THREAD, the thread isolation strategy for Hystrix is used for all routes. +In that case, the HystrixThreadPoolKey is set to RibbonCommand as the default. +It means that HystrixCommands for all routes are executed in the same Hystrix thread pool. +This behavior can be changed with the following configuration:

    application.yml.  +

    zuul:
    +  threadPool:
    +    useSeparateThreadPools: true

    +

    The preceding example results in HystrixCommands being executed in the Hystrix thread pool for each route.

    In this case, the default HystrixThreadPoolKey is the same as the service ID for each route. +To add a prefix to HystrixThreadPoolKey, set zuul.threadPool.threadPoolKeyPrefix to the value that you want to add, as shown in the following example:

    application.yml.  +

    zuul:
    +  threadPool:
    +    useSeparateThreadPools: true
    +    threadPoolKeyPrefix: zuulgw

    +

    16.11 How to Provide a Key to Ribbon’s IRule

    If you need to provide your own IRule implementation to handle a special routing requirement like a canary test, pass some information to the choose method of IRule.

    com.netflix.loadbalancer.IRule.java.  +

    public interface IRule{
    +    public Server choose(Object key);
    +         :

    +

    You can provide some information that is used by your IRule implementation to choose a target server, as shown in the following example:

    RequestContext.getCurrentContext()
    +              .set(FilterConstants.LOAD_BALANCER_KEY, "canary-test");

    If you put any object into the RequestContext with a key of FilterConstants.LOAD_BALANCER_KEY, it is passed to the choose method of the IRule implementation. +The code shown in the preceding example must be executed before RibbonRoutingFilter is executed. +Zuul’s pre filter is the best place to do that. +You can access HTTP headers and query parameters through the RequestContext in pre filter, so it can be used to determine the LOAD_BALANCER_KEY that is passed to Ribbon. +If you do not put any value with LOAD_BALANCER_KEY in RequestContext, null is passed as a parameter of the choose method.

    17. External Configuration: Archaius

    Archaius is the Netflix client-side configuration library. +It is the library used by all of the Netflix OSS components for configuration. +Archaius is an extension of the Apache Commons Configuration project. +It allows updates to configuration by either polling a source for changes or by letting a source push changes to the client. +Archaius uses Dynamic<Type>Property classes as handles to properties, as shown in the following example:

    Archaius Example.  +

    class ArchaiusTest {
    +    DynamicStringProperty myprop = DynamicPropertyFactory
    +            .getInstance()
    +            .getStringProperty("my.prop");
    +
    +    void doSomething() {
    +        OtherClass.someMethod(myprop.get());
    +    }
    +}

    +

    Archaius has its own set of configuration files and loading priorities. +Spring applications should generally not use Archaius directly, but the need to configure the Netflix tools natively remains. +Spring Cloud has a Spring Environment Bridge so that Archaius can read properties from the Spring Environment. +This bridge allows Spring Boot projects to use the normal configuration toolchain while letting them configure the Netflix tools as documented (for the most part).

    18. Router and Filter: Zuul

    Routing is an integral part of a microservice architecture. +For example, / may be mapped to your web application, /api/users is mapped to the user service and /api/shop is mapped to the shop service. +Zuul is a JVM-based router and server-side load balancer from Netflix.

    Netflix uses Zuul for the following:

    • Authentication
    • Insights
    • Stress Testing
    • Canary Testing
    • Dynamic Routing
    • Service Migration
    • Load Shedding
    • Security
    • Static Response handling
    • Active/Active traffic management

    Zuul’s rule engine lets rules and filters be written in essentially any JVM language, with built-in support for Java and Groovy.

    [Note]Note

    The configuration property zuul.max.host.connections has been replaced by two new properties, zuul.host.maxTotalConnections and zuul.host.maxPerRouteConnections, which default to 200 and 20 respectively.

    [Note]Note

    The default Hystrix isolation pattern (ExecutionIsolationStrategy) for all routes is SEMAPHORE. +zuul.ribbonIsolationStrategy can be changed to THREAD if that isolation pattern is preferred.

    18.1 How to Include Zuul

    To include Zuul in your project, use the starter with a group ID of org.springframework.cloud and a artifact ID of spring-cloud-starter-netflix-zuul. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    18.2 Embedded Zuul Reverse Proxy

    Spring Cloud has created an embedded Zuul proxy to ease the development of a common use case where a UI application wants to make proxy calls to one or more back end services. +This feature is useful for a user interface to proxy to the back end services it requires, avoiding the need to manage CORS and authentication concerns independently for all the back ends.

    To enable it, annotate a Spring Boot main class with @EnableZuulProxy. Doing so causes local calls to be forwarded to the appropriate service. +By convention, a service with an ID of users receives requests from the proxy located at /users (with the prefix stripped). +The proxy uses Ribbon to locate an instance to which to forward through discovery. +All requests are executed in a hystrix command, so failures appear in Hystrix metrics. +Once the circuit is open, the proxy does not try to contact the service.

    [Note]Note

    the Zuul starter does not include a discovery client, so, for routes based on service IDs, you need to provide one of those on the classpath as well (Eureka is one choice).

    To skip having a service automatically added, set zuul.ignored-services to a list of service ID patterns. +If a service matches a pattern that is ignored but is also included in the explicitly configured routes map, it is unignored, as shown in the following example:

    application.yml.  +

     zuul:
    +  ignoredServices: '*'
    +  routes:
    +    users: /myusers/**

    +

    In the preceding example, all services are ignored, except for users.

    To augment or change the proxy routes, you can add external configuration, as follows:

    application.yml.  +

     zuul:
    +  routes:
    +    users: /myusers/**

    +

    The preceding example means that HTTP calls to /myusers get forwarded to the users service (for example /myusers/101 is forwarded to /101).

    To get more fine-grained control over a route, you can specify the path and the serviceId independently, as follows:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      serviceId: users_service

    +

    The preceding example means that HTTP calls to /myusers get forwarded to the users_service service. +The route must have a path that can be specified as an ant-style pattern, so /myusers/* only matches one level, but /myusers/** matches hierarchically.

    The location of the back end can be specified as either a serviceId (for a service from discovery) or a url (for a physical location), as shown in the following example:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      url: https://example.com/users_service

    +

    These simple url-routes do not get executed as a HystrixCommand, nor do they load-balance multiple URLs with Ribbon. +To achieve those goals, you can specify a serviceId with a static list of servers, as follows:

    application.yml.  +

    zuul:
    +  routes:
    +    echo:
    +      path: /myusers/**
    +      serviceId: myusers-service
    +      stripPrefix: true
    +
    +hystrix:
    +  command:
    +    myusers-service:
    +      execution:
    +        isolation:
    +          thread:
    +            timeoutInMilliseconds: ...
    +
    +myusers-service:
    +  ribbon:
    +    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    +    listOfServers: https://example1.com,http://example2.com
    +    ConnectTimeout: 1000
    +    ReadTimeout: 3000
    +    MaxTotalHttpConnections: 500
    +    MaxConnectionsPerHost: 100

    +

    Another method is specifiying a service-route and configuring a Ribbon client for the serviceId (doing so requires disabling Eureka support in Ribbon — see above for more information), as shown in the following example:

    application.yml.  +

    zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      serviceId: users
    +
    +ribbon:
    +  eureka:
    +    enabled: false
    +
    +users:
    +  ribbon:
    +    listOfServers: example.com,google.com

    +

    You can provide a convention between serviceId and routes by using regexmapper. +It uses regular-expression named groups to extract variables from serviceId and inject them into a route pattern, as shown in the following example:

    ApplicationConfiguration.java.  +

    @Bean
    +public PatternServiceRouteMapper serviceRouteMapper() {
    +    return new PatternServiceRouteMapper(
    +        "(?<name>^.+)-(?<version>v.+$)",
    +        "${version}/${name}");
    +}

    +

    The preceding example means that a serviceId of myusers-v1 is mapped to route /v1/myusers/**. +Any regular expression is accepted, but all named groups must be present in both servicePattern and routePattern. +If servicePattern does not match a serviceId, the default behavior is used. +In the preceding example, a serviceId of myusers is mapped to the "/myusers/**" route (with no version detected). +This feature is disabled by default and only applies to discovered services.

    To add a prefix to all mappings, set zuul.prefix to a value, such as /api. +By default, the proxy prefix is stripped from the request before the request is forwarded by (you can switch this behavior off with zuul.stripPrefix=false). +You can also switch off the stripping of the service-specific prefix from individual routes, as shown in the following example:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      stripPrefix: false

    +

    [Note]Note

    zuul.stripPrefix only applies to the prefix set in zuul.prefix. +It does not have any effect on prefixes defined within a given route’s path.

    In the preceding example, requests to /myusers/101 are forwarded to /myusers/101 on the users service.

    The zuul.routes entries actually bind to an object of type ZuulProperties. +If you look at the properties of that object, you can see that it also has a retryable flag. +Set that flag to true to have the Ribbon client automatically retry failed requests. +You can also set that flag to true when you need to modify the parameters of the retry operations that use the Ribbon client configuration.

    By default, the X-Forwarded-Host header is added to the forwarded requests. +To turn it off, set zuul.addProxyHeaders = false. +By default, the prefix path is stripped, and the request to the back end picks up a X-Forwarded-Prefix header (/myusers in the examples shown earlier).

    If you set a default route (/), an application with @EnableZuulProxy could act as a standalone server. For example, zuul.route.home: / would route all traffic ("/**") to the "home" service.

    If more fine-grained ignoring is needed, you can specify specific patterns to ignore. +These patterns are evaluated at the start of the route location process, which means prefixes should be included in the pattern to warrant a match. +Ignored patterns span all services and supersede any other route specification. +The following example shows how to create ignored patterns:

    application.yml.  +

     zuul:
    +  ignoredPatterns: /**/admin/**
    +  routes:
    +    users: /myusers/**

    +

    The preceding example means that all calls (such as /myusers/101) are forwarded to /101 on the users service. +However, calls including /admin/ do not resolve.

    [Warning]Warning

    If you need your routes to have their order preserved, you need to use a YAML file, as the ordering is lost when using a properties file. +The following example shows such a YAML file:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +    legacy:
    +      path: /**

    +

    If you were to use a properties file, the legacy path might end up in front of the users +path, rendering the users path unreachable.

    18.3 Zuul Http Client

    The default HTTP client used by Zuul is now backed by the Apache HTTP Client instead of the deprecated Ribbon RestClient. +To use RestClient or okhttp3.OkHttpClient, set ribbon.restclient.enabled=true or ribbon.okhttp.enabled=true, respectively. +If you would like to customize the Apache HTTP client or the OK HTTP client, provide a bean of type ClosableHttpClient or OkHttpClient.

    18.4 Cookies and Sensitive Headers

    You can share headers between services in the same system, but you probably do not want sensitive headers leaking downstream into external servers. +You can specify a list of ignored headers as part of the route configuration. +Cookies play a special role, because they have well defined semantics in browsers, and they are always to be treated as sensitive. +If the consumer of your proxy is a browser, then cookies for downstream services also cause problems for the user, because they all get jumbled up together (all downstream services look like they come from the same place).

    If you are careful with the design of your services, (for example, if only one of the downstream services sets cookies), you might be able to let them flow from the back end all the way up to the caller. +Also, if your proxy sets cookies and all your back-end services are part of the same system, it can be natural to simply share them (and, for instance, use Spring Session to link them up to some shared state). +Other than that, any cookies that get set by downstream services are likely to be not useful to the caller, so it is recommended that you make (at least) Set-Cookie and Cookie into sensitive headers for routes that are not part of your domain. +Even for routes that are part of your domain, try to think carefully about what it means before letting cookies flow between them and the proxy.

    The sensitive headers can be configured as a comma-separated list per route, as shown in the following example:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      sensitiveHeaders: Cookie,Set-Cookie,Authorization
    +      url: https://downstream

    +

    [Note]Note

    This is the default value for sensitiveHeaders, so you need not set it unless you want it to be different. +This is new in Spring Cloud Netflix 1.1 (in 1.0, the user had no control over headers, and all cookies flowed in both directions).

    The sensitiveHeaders are a blacklist, and the default is not empty. +Consequently, to make Zuul send all headers (except the ignored ones), you must explicitly set it to the empty list. +Doing so is necessary if you want to pass cookie or authorization headers to your back end. The following example shows how to use sensitiveHeaders:

    application.yml.  +

     zuul:
    +  routes:
    +    users:
    +      path: /myusers/**
    +      sensitiveHeaders:
    +      url: https://downstream

    +

    You can also set sensitive headers, by setting zuul.sensitiveHeaders. +If sensitiveHeaders is set on a route, it overrides the global sensitiveHeaders setting.

    18.5 Ignored Headers

    In addition to the route-sensitive headers, you can set a global value called zuul.ignoredHeaders for values (both request and response) that should be discarded during interactions with downstream services. +By default, if Spring Security is not on the classpath, these are empty. +Otherwise, they are initialized to a set of well known security headers (for example, involving caching) as specified by Spring Security. +The assumption in this case is that the downstream services might add these headers, too, but we want the values from the proxy. +To not discard these well known security headers when Spring Security is on the classpath, you can set zuul.ignoreSecurityHeaders to false. +Doing so can be useful if you disabled the HTTP Security response headers in Spring Security and want the values provided by downstream services.

    18.6 Management Endpoints

    By default, if you use @EnableZuulProxy with the Spring Boot Actuator, you enable two additional endpoints:

    • Routes
    • Filters

    18.6.1 Routes Endpoint

    A GET to the routes endpoint at /routes returns a list of the mapped routes:

    GET /routes.  +

    {
    +  /stores/**: "http://localhost:8081"
    +}

    +

    Additional route details can be requested by adding the ?format=details query string to /routes. +Doing so produces the following output:

    GET /routes/details.  +

    {
    +  "/stores/**": {
    +    "id": "stores",
    +    "fullPath": "/stores/**",
    +    "location": "http://localhost:8081",
    +    "path": "/**",
    +    "prefix": "/stores",
    +    "retryable": false,
    +    "customSensitiveHeaders": false,
    +    "prefixStripped": true
    +  }
    +}

    +

    A POST to /routes forces a refresh of the existing routes (for example, when there have been changes in the service catalog). +You can disable this endpoint by setting endpoints.routes.enabled to false.

    [Note]Note

    the routes should respond automatically to changes in the service catalog, but the POST to /routes is a way to force the change +to happen immediately.

    18.6.2 Filters Endpoint

    A GET to the filters endpoint at /filters returns a map of Zuul filters by type. +For each filter type in the map, you get a list of all the filters of that type, along with their details.

    18.7 Strangulation Patterns and Local Forwards

    A common pattern when migrating an existing application or API is to strangle old endpoints, slowly replacing them with different implementations. +The Zuul proxy is a useful tool for this because you can use it to handle all traffic from the clients of the old endpoints but redirect some of the requests to new ones.

    The following example shows the configuration details for a strangle scenario:

    application.yml.  +

     zuul:
    +  routes:
    +    first:
    +      path: /first/**
    +      url: https://first.example.com
    +    second:
    +      path: /second/**
    +      url: forward:/second
    +    third:
    +      path: /third/**
    +      url: forward:/3rd
    +    legacy:
    +      path: /**
    +      url: https://legacy.example.com

    +

    In the preceding example, we are strangle the legacy application, which is mapped to all requests that do not match one of the other patterns. +Paths in /first/** have been extracted into a new service with an external URL. +Paths in /second/** are forwarded so that they can be handled locally (for example, with a normal Spring @RequestMapping). +Paths in /third/** are also forwarded but with a different prefix (/third/foo is forwarded to /3rd/foo).

    [Note]Note

    The ignored patterns aren’t completely ignored, they just are not handled by the proxy (so they are also effectively forwarded locally).

    18.8 Uploading Files through Zuul

    If you use @EnableZuulProxy, you can use the proxy paths to upload files and it should work, so long as the files are small. +For large files there is an alternative path that bypasses the Spring DispatcherServlet (to avoid multipart processing) in "/zuul/*". +In other words, if you have zuul.routes.customers=/customers/**, then you can POST large files to /zuul/customers/*. +The servlet path is externalized via zuul.servletPath. +If the proxy route takes you through a Ribbon load balancer, extremely large files also require elevated timeout settings, as shown in the following example:

    application.yml.  +

    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
    +ribbon:
    +  ConnectTimeout: 3000
    +  ReadTimeout: 60000

    +

    Note that, for streaming to work with large files, you need to use chunked encoding in the request (which some browsers do not do by default), as shown in the following example:

    $ curl -v -H "Transfer-Encoding: chunked" \
    +    -F "file=@mylarge.iso" localhost:9999/zuul/simple/file

    18.9 Query String Encoding

    When processing the incoming request, query params are decoded so that they can be available for possible modifications in Zuul filters. +They are then re-encoded the back end request is rebuilt in the route filters. +The result can be different than the original input if (for example) it was encoded with Javascript’s encodeURIComponent() method. +While this causes no issues in most cases, some web servers can be picky with the encoding of complex query string.

    To force the original encoding of the query string, it is possible to pass a special flag to ZuulProperties so that the query string is taken as is with the HttpServletRequest::getQueryString method, as shown in the following example:

    application.yml.  +

     zuul:
    +  forceOriginalQueryStringEncoding: true

    +

    [Note]Note

    This special flag works only with SimpleHostRoutingFilter. Also, you loose the ability to easily override +query parameters with RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters), because +the query string is now fetched directly on the original HttpServletRequest.

    18.10 Request URI Encoding

    When processing the incoming request, request URI is decoded before matching them to routes. +The request URI is then re-encoded when the back end request is rebuilt in the route filters. +This can cause some unexpected behavior if your URI includes the encoded "/" character.

    To use the original request URI, it is possible to pass a special flag to 'ZuulProperties' so that the URI will be taken as is with the HttpServletRequest::getRequestURI method, as shown in the following example:

    application.yml.  +

     zuul:
    +  decodeUrl: false

    +

    [Note]Note

    If you are overriding request URI using requestURI RequestContext attribute and this flag is set to false, then the URL set in the request context will not be encoded. It will be your responsibility to make sure the URL is already encoded.

    18.11 Plain Embedded Zuul

    If you use @EnableZuulServer (instead of @EnableZuulProxy), you can also run a Zuul server without proxying or selectively switch on parts of the proxying platform. +Any beans that you add to the application of type ZuulFilter are installed automatically (as they are with @EnableZuulProxy) but without any of the proxy filters being added automatically.

    In that case, the routes into the Zuul server are still specified by configuring "zuul.routes.*", but there is no service discovery and no proxying. Consequently, the "serviceId" and "url" settings are ignored. +The following example maps all paths in "/api/**" to the Zuul filter chain:

    application.yml.  +

     zuul:
    +  routes:
    +    api: /api/**

    +

    18.12 Disable Zuul Filters

    Zuul for Spring Cloud comes with a number of ZuulFilter beans enabled by default in both proxy and server mode. +See the Zuul filters package for the list of filters that you can enable. +If you want to disable one, set zuul.<SimpleClassName>.<filterType>.disable=true. +By convention, the package after filters is the Zuul filter type. +For example to disable org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter, set zuul.SendResponseFilter.post.disable=true.

    18.13 Providing Hystrix Fallbacks For Routes

    When a circuit for a given route in Zuul is tripped, you can provide a fallback response by creating a bean of type FallbackProvider. +Within this bean, you need to specify the route ID the fallback is for and provide a ClientHttpResponse to return as a fallback. +The following example shows a relatively simple FallbackProvider implementation:

    class MyFallbackProvider implements FallbackProvider {
    +
    +    @Override
    +    public String getRoute() {
    +        return "customers";
    +    }
    +
    +    @Override
    +    public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
    +        if (cause instanceof HystrixTimeoutException) {
    +            return response(HttpStatus.GATEWAY_TIMEOUT);
    +        } else {
    +            return response(HttpStatus.INTERNAL_SERVER_ERROR);
    +        }
    +    }
    +
    +    private ClientHttpResponse response(final HttpStatus status) {
    +        return new ClientHttpResponse() {
    +            @Override
    +            public HttpStatus getStatusCode() throws IOException {
    +                return status;
    +            }
    +
    +            @Override
    +            public int getRawStatusCode() throws IOException {
    +                return status.value();
    +            }
    +
    +            @Override
    +            public String getStatusText() throws IOException {
    +                return status.getReasonPhrase();
    +            }
    +
    +            @Override
    +            public void close() {
    +            }
    +
    +            @Override
    +            public InputStream getBody() throws IOException {
    +                return new ByteArrayInputStream("fallback".getBytes());
    +            }
    +
    +            @Override
    +            public HttpHeaders getHeaders() {
    +                HttpHeaders headers = new HttpHeaders();
    +                headers.setContentType(MediaType.APPLICATION_JSON);
    +                return headers;
    +            }
    +        };
    +    }
    +}

    The following example shows how the route configuration for the previous example might appear:

    zuul:
    +  routes:
    +    customers: /customers/**

    If you would like to provide a default fallback for all routes, you can create a bean of type FallbackProvider and have the getRoute method return * or null, as shown in the following example:

    class MyFallbackProvider implements FallbackProvider {
    +    @Override
    +    public String getRoute() {
    +        return "*";
    +    }
    +
    +    @Override
    +    public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
    +        return new ClientHttpResponse() {
    +            @Override
    +            public HttpStatus getStatusCode() throws IOException {
    +                return HttpStatus.OK;
    +            }
    +
    +            @Override
    +            public int getRawStatusCode() throws IOException {
    +                return 200;
    +            }
    +
    +            @Override
    +            public String getStatusText() throws IOException {
    +                return "OK";
    +            }
    +
    +            @Override
    +            public void close() {
    +
    +            }
    +
    +            @Override
    +            public InputStream getBody() throws IOException {
    +                return new ByteArrayInputStream("fallback".getBytes());
    +            }
    +
    +            @Override
    +            public HttpHeaders getHeaders() {
    +                HttpHeaders headers = new HttpHeaders();
    +                headers.setContentType(MediaType.APPLICATION_JSON);
    +                return headers;
    +            }
    +        };
    +    }
    +}

    18.14 Zuul Timeouts

    If you want to configure the socket timeouts and read timeouts for requests proxied through Zuul, you have two options, based on your configuration:

    • If Zuul uses service discovery, you need to configure these timeouts with the +ribbon.ReadTimeout and ribbon.SocketTimeout Ribbon properties.

    If you have configured Zuul routes by specifying URLs, you need to use +zuul.host.connect-timeout-millis and zuul.host.socket-timeout-millis.

    18.15 Rewriting the Location header

    If Zuul is fronting a web application, you may need to re-write the Location header when the web application redirects through a HTTP status code of 3XX. +Otherwise, the browser redirects to the web application’s URL instead of the Zuul URL. +You can configure a LocationRewriteFilter Zuul filter to re-write the Location header to the Zuul’s URL. +It also adds back the stripped global and route-specific prefixes. +The following example adds a filter by using a Spring Configuration file:

    import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
    +...
    +
    +@Configuration
    +@EnableZuulProxy
    +public class ZuulConfig {
    +    @Bean
    +    public LocationRewriteFilter locationRewriteFilter() {
    +        return new LocationRewriteFilter();
    +    }
    +}
    [Caution]Caution

    Use this filter carefully. The filter acts on the Location header of ALL 3XX response codes, which may not be appropriate in all scenarios, such as when redirecting the user to an external URL.

    18.16 Enabling Cross Origin Requests

    By default Zuul routes all Cross Origin requests (CORS) to the services. If you want instead Zuul to handle these requests it can be done by providing custom WebMvcConfigurer bean:

    @Bean
    +public WebMvcConfigurer corsConfigurer() {
    +    return new WebMvcConfigurer() {
    +        public void addCorsMappings(CorsRegistry registry) {
    +            registry.addMapping("/path-1/**")
    +                    .allowedOrigins("https://allowed-origin.com")
    +                    .allowedMethods("GET", "POST");
    +        }
    +    };
    +}

    In the example above, we allow GET and POST methods from https://allowed-origin.com to send cross-origin requests to the endpoints starting with path-1. +You can apply CORS configuration to a specific path pattern or globally for the whole application, using /** mapping. +You can customize properties: allowedOrigins,allowedMethods,allowedHeaders,exposedHeaders,allowCredentials and maxAge via this configuration.

    18.17 Metrics

    Zuul will provide metrics under the Actuator metrics endpoint for any failures that might occur when routing requests. +These metrics can be viewed by hitting /actuator/metrics. The metrics will have a name that has the format +ZUUL::EXCEPTION:errorCause:statusCode.

    18.18 Zuul Developer Guide

    For a general overview of how Zuul works, see the Zuul Wiki.

    18.18.1 The Zuul Servlet

    Zuul is implemented as a Servlet. For the general cases, Zuul is embedded into the Spring Dispatch mechanism. This lets Spring MVC be in control of the routing. +In this case, Zuul buffers requests. +If there is a need to go through Zuul without buffering requests (for example, for large file uploads), the Servlet is also installed outside of the Spring Dispatcher. +By default, the servlet has an address of /zuul. +This path can be changed with the zuul.servlet-path property.

    18.18.2 Zuul RequestContext

    To pass information between filters, Zuul uses a RequestContext. +Its data is held in a ThreadLocal specific to each request. +Information about where to route requests, errors, and the actual HttpServletRequest and HttpServletResponse are stored there. +The RequestContext extends ConcurrentHashMap, so anything can be stored in the context. FilterConstants contains the keys used by the filters installed by Spring Cloud Netflix (more on these later).

    18.18.3 @EnableZuulProxy vs. @EnableZuulServer

    Spring Cloud Netflix installs a number of filters, depending on which annotation was used to enable Zuul. @EnableZuulProxy is a superset of @EnableZuulServer. In other words, @EnableZuulProxy contains all the filters installed by @EnableZuulServer. The additional filters in the proxy enable routing functionality. If you want a blank Zuul, you should use @EnableZuulServer.

    18.18.4 @EnableZuulServer Filters

    @EnableZuulServer creates a SimpleRouteLocator that loads route definitions from Spring Boot configuration files.

    The following filters are installed (as normal Spring Beans):

    • Pre filters:

      • ServletDetectionFilter: Detects whether the request is through the Spring Dispatcher. Sets a boolean with a key of FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY.
      • FormBodyWrapperFilter: Parses form data and re-encodes it for downstream requests.
      • DebugFilter: If the debug request parameter is set, sets RequestContext.setDebugRouting() and RequestContext.setDebugRequest() to true. +*Route filters:
      • SendForwardFilter: Forwards requests by using the Servlet RequestDispatcher. The forwarding location is stored in the RequestContext attribute, FilterConstants.FORWARD_TO_KEY. This is useful for forwarding to endpoints in the current application.
    • Post filters:

      • SendResponseFilter: Writes responses from proxied requests to the current response.
    • Error filters:

      • SendErrorFilter: Forwards to /error (by default) if RequestContext.getThrowable() is not null. You can change the default forwarding path (/error) by setting the error.path property.

    18.18.5 @EnableZuulProxy Filters

    Creates a DiscoveryClientRouteLocator that loads route definitions from a DiscoveryClient (such as Eureka) as well as from properties. A route is created for each serviceId from the DiscoveryClient. As new services are added, the routes are refreshed.

    In addition to the filters described earlier, the following filters are installed (as normal Spring Beans):

    • Pre filters:

      • PreDecorationFilter: Determines where and how to route, depending on the supplied RouteLocator. It also sets various proxy-related headers for downstream requests.
    • Route filters:

      • RibbonRoutingFilter: Uses Ribbon, Hystrix, and pluggable HTTP clients to send requests. Service IDs are found in the RequestContext attribute, FilterConstants.SERVICE_ID_KEY. This filter can use different HTTP clients:

        • Apache HttpClient: The default client.
        • Squareup OkHttpClient v3: Enabled by having the com.squareup.okhttp3:okhttp library on the classpath and setting ribbon.okhttp.enabled=true.
        • Netflix Ribbon HTTP client: Enabled by setting ribbon.restclient.enabled=true. This client has limitations, including that it does not support the PATCH method, but it also has built-in retry.
      • SimpleHostRoutingFilter: Sends requests to predetermined URLs through an Apache HttpClient. URLs are found in RequestContext.getRouteHost().

    18.18.6 Custom Zuul Filter Examples

    Most of the following "How to Write" examples below are included Sample Zuul Filters project. There are also examples of manipulating the request or response body in that repository.

    This section includes the following examples:

    How to Write a Pre Filter

    Pre filters set up data in the RequestContext for use in filters downstream. +The main use case is to set information required for route filters. +The following example shows a Zuul pre filter:

    public class QueryParamPreFilter extends ZuulFilter {
    +	@Override
    +	public int filterOrder() {
    +		return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
    +	}
    +
    +	@Override
    +	public String filterType() {
    +		return PRE_TYPE;
    +	}
    +
    +	@Override
    +	public boolean shouldFilter() {
    +		RequestContext ctx = RequestContext.getCurrentContext();
    +		return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
    +				&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
    +	}
    +    @Override
    +    public Object run() {
    +        RequestContext ctx = RequestContext.getCurrentContext();
    +		HttpServletRequest request = ctx.getRequest();
    +		if (request.getParameter("sample") != null) {
    +		    // put the serviceId in `RequestContext`
    +    		ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
    +    	}
    +        return null;
    +    }
    +}

    The preceding filter populates SERVICE_ID_KEY from the sample request parameter. +In practice, you should not do that kind of direct mapping. Instead, the service ID should be looked up from the value of sample instead.

    Now that SERVICE_ID_KEY is populated, PreDecorationFilter does not run and RibbonRoutingFilter runs.

    [Tip]Tip

    If you want to route to a full URL, call ctx.setRouteHost(url) instead.

    To modify the path to which routing filters forward, set the REQUEST_URI_KEY.

    How to Write a Route Filter

    Route filters run after pre filters and make requests to other services. +Much of the work here is to translate request and response data to and from the model required by the client. +The following example shows a Zuul route filter:

    public class OkHttpRoutingFilter extends ZuulFilter {
    +	@Autowired
    +	private ProxyRequestHelper helper;
    +
    +	@Override
    +	public String filterType() {
    +		return ROUTE_TYPE;
    +	}
    +
    +	@Override
    +	public int filterOrder() {
    +		return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
    +	}
    +
    +	@Override
    +	public boolean shouldFilter() {
    +		return RequestContext.getCurrentContext().getRouteHost() != null
    +				&& RequestContext.getCurrentContext().sendZuulResponse();
    +	}
    +
    +    @Override
    +    public Object run() {
    +		OkHttpClient httpClient = new OkHttpClient.Builder()
    +				// customize
    +				.build();
    +
    +		RequestContext context = RequestContext.getCurrentContext();
    +		HttpServletRequest request = context.getRequest();
    +
    +		String method = request.getMethod();
    +
    +		String uri = this.helper.buildZuulRequestURI(request);
    +
    +		Headers.Builder headers = new Headers.Builder();
    +		Enumeration<String> headerNames = request.getHeaderNames();
    +		while (headerNames.hasMoreElements()) {
    +			String name = headerNames.nextElement();
    +			Enumeration<String> values = request.getHeaders(name);
    +
    +			while (values.hasMoreElements()) {
    +				String value = values.nextElement();
    +				headers.add(name, value);
    +			}
    +		}
    +
    +		InputStream inputStream = request.getInputStream();
    +
    +		RequestBody requestBody = null;
    +		if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
    +			MediaType mediaType = null;
    +			if (headers.get("Content-Type") != null) {
    +				mediaType = MediaType.parse(headers.get("Content-Type"));
    +			}
    +			requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
    +		}
    +
    +		Request.Builder builder = new Request.Builder()
    +				.headers(headers.build())
    +				.url(uri)
    +				.method(method, requestBody);
    +
    +		Response response = httpClient.newCall(builder.build()).execute();
    +
    +		LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
    +
    +		for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
    +			responseHeaders.put(entry.getKey(), entry.getValue());
    +		}
    +
    +		this.helper.setResponse(response.code(), response.body().byteStream(),
    +				responseHeaders);
    +		context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
    +		return null;
    +    }
    +}

    The preceding filter translates Servlet request information into OkHttp3 request information, executes an HTTP request, and translates OkHttp3 response information to the Servlet response.

    How to Write a Post Filter

    Post filters typically manipulate the response. The following filter adds a random UUID as the X-Sample header:

    public class AddResponseHeaderFilter extends ZuulFilter {
    +	@Override
    +	public String filterType() {
    +		return POST_TYPE;
    +	}
    +
    +	@Override
    +	public int filterOrder() {
    +		return SEND_RESPONSE_FILTER_ORDER - 1;
    +	}
    +
    +	@Override
    +	public boolean shouldFilter() {
    +		return true;
    +	}
    +
    +	@Override
    +	public Object run() {
    +		RequestContext context = RequestContext.getCurrentContext();
    +    	HttpServletResponse servletResponse = context.getResponse();
    +		servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
    +		return null;
    +	}
    +}
    [Note]Note

    Other manipulations, such as transforming the response body, are much more complex and computationally intensive.

    18.18.7 How Zuul Errors Work

    If an exception is thrown during any portion of the Zuul filter lifecycle, the error filters are executed. +The SendErrorFilter is only run if RequestContext.getThrowable() is not null. +It then sets specific javax.servlet.error.* attributes in the request and forwards the request to the Spring Boot error page.

    18.18.8 Zuul Eager Application Context Loading

    Zuul internally uses Ribbon for calling the remote URLs. +By default, Ribbon clients are lazily loaded by Spring Cloud on first call. +This behavior can be changed for Zuul by using the following configuration, which results eager loading of the child Ribbon related Application contexts at application startup time. +The following example shows how to enable eager loading:

    application.yml.  +

    zuul:
    +  ribbon:
    +    eager-load:
    +      enabled: true

    +

    19. Polyglot support with Sidecar

    Do you have non-JVM languages with which you want to take advantage of Eureka, Ribbon, and Config Server? +The Spring Cloud Netflix Sidecar was inspired by Netflix Prana. +It includes an HTTP API to get all of the instances (by host and port) for a given service. +You can also proxy service calls through an embedded Zuul proxy that gets its route entries from Eureka. +The Spring Cloud Config Server can be accessed directly through host lookup or through the Zuul Proxy. +The non-JVM application should implement a health check so the Sidecar can report to Eureka whether the app is up or down.

    To include Sidecar in your project, use the dependency with a group ID of org.springframework.cloud +and artifact ID or spring-cloud-netflix-sidecar.

    To enable the Sidecar, create a Spring Boot application with @EnableSidecar. +This annotation includes @EnableCircuitBreaker, @EnableDiscoveryClient, and @EnableZuulProxy. +Run the resulting application on the same host as the non-JVM application.

    To configure the side car, add sidecar.port and sidecar.health-uri to application.yml. +The sidecar.port property is the port on which the non-JVM application listens. +This is so the Sidecar can properly register the application with Eureka. +The sidecar.secure-port-enabled options provides a way to enable secure port for traffic. +The sidecar.health-uri is a URI accessible on the non-JVM application that mimics a Spring Boot health indicator. +It should return a JSON document that resembles the following:

    health-uri-document.  +

    {
    +  "status":"UP"
    +}

    +

    The following application.yml example shows sample configuration for a Sidecar application:

    application.yml.  +

    server:
    +  port: 5678
    +spring:
    +  application:
    +    name: sidecar
    +
    +sidecar:
    +  port: 8000
    +  health-uri: http://localhost:8000/health.json

    +

    The API for the DiscoveryClient.getInstances() method is /hosts/{serviceId}. +The following example response for /hosts/customers returns two instances on different hosts:

    /hosts/customers.  +

    [
    +    {
    +        "host": "myhost",
    +        "port": 9000,
    +        "uri": "http://myhost:9000",
    +        "serviceId": "CUSTOMERS",
    +        "secure": false
    +    },
    +    {
    +        "host": "myhost2",
    +        "port": 9000,
    +        "uri": "http://myhost2:9000",
    +        "serviceId": "CUSTOMERS",
    +        "secure": false
    +    }
    +]

    +

    This API is accessible to the non-JVM application (if the sidecar is on port 5678) at http://localhost:5678/hosts/{serviceId}.

    The Zuul proxy automatically adds routes for each service known in Eureka to /<serviceId>, so the customers service is available at /customers. +The non-JVM application can access the customer service at http://localhost:5678/customers (assuming the sidecar is listening on port 5678).

    If the Config Server is registered with Eureka, the non-JVM application can access it through the Zuul proxy. +If the serviceId of the ConfigServer is configserver and the Sidecar is on port 5678, then it can be accessed at http://localhost:5678/configserver.

    Non-JVM applications can take advantage of the Config Server’s ability to return YAML documents. +For example, a call to https://sidecar.local.spring.io:5678/configserver/default-master.yml +might result in a YAML document resembling the following:

    eureka:
    +  client:
    +    serviceUrl:
    +      defaultZone: http://localhost:8761/eureka/
    +  password: password
    +info:
    +  description: Spring Cloud Samples
    +  url: https://github.com/spring-cloud-samples

    To enable the health check request to accept all certificates when using HTTPs set sidecar.accept-all-ssl-certificates to `true.

    20. Retrying Failed Requests

    Spring Cloud Netflix offers a variety of ways to make HTTP requests. +You can use a load balanced RestTemplate, Ribbon, or Feign. +No matter how you choose to create your HTTP requests, there is always a chance that a request may fail. +When a request fails, you may want to have the request be retried automatically. +To do so when using Sping Cloud Netflix, you need to include Spring Retry on your application’s classpath. +When Spring Retry is present, load-balanced RestTemplates, Feign, and Zuul automatically retry any failed requests (assuming your configuration allows doing so).

    20.1 BackOff Policies

    By default, no backoff policy is used when retrying requests. +If you would like to configure a backoff policy, you need to create a bean of type LoadBalancedRetryFactory and override the createBackOffPolicy method for a given service, as shown in the following example:

    @Configuration
    +public class MyConfiguration {
    +    @Bean
    +    LoadBalancedRetryFactory retryFactory() {
    +        return new LoadBalancedRetryFactory() {
    +            @Override
    +            public BackOffPolicy createBackOffPolicy(String service) {
    +                return new ExponentialBackOffPolicy();
    +            }
    +        };
    +    }
    +}

    20.2 Configuration

    When you use Ribbon with Spring Retry, you can control the retry functionality by configuring certain Ribbon properties. +To do so, set the client.ribbon.MaxAutoRetries, client.ribbon.MaxAutoRetriesNextServer, and client.ribbon.OkToRetryOnAllOperations properties. +See the Ribbon documentation for a description of what these properties do.

    [Warning]Warning

    Enabling client.ribbon.OkToRetryOnAllOperations includes retrying POST requests, which can have an impact +on the server’s resources, due to the buffering of the request body.

    In addition, you may want to retry requests when certain status codes are returned in the response. +You can list the response codes you would like the Ribbon client to retry by setting the clientName.ribbon.retryableStatusCodes property, as shown in the following example:

    clientName:
    +  ribbon:
    +    retryableStatusCodes: 404,502

    You can also create a bean of type LoadBalancedRetryPolicy and implement the retryableStatusCode method to retry a request given the status code.

    20.2.1 Zuul

    You can turn off Zuul’s retry functionality by setting zuul.retryable to false. +You can also disable retry functionality on a route-by-route basis by setting zuul.routes.routename.retryable to false.

    21. HTTP Clients

    Spring Cloud Netflix automatically creates the HTTP client used by Ribbon, Feign, and Zuul for you. +However, you can also provide your own HTTP clients customized as you need them to be. +To do so, you can create a bean of type ClosableHttpClient if you +are using the Apache Http Cient or OkHttpClient if you are using OK HTTP.

    [Note]Note

    When you create your own HTTP client, you are also responsible for implementing the correct connection management strategies for these clients. +Doing so improperly can result in resource management issues.

    22. Modules In Maintenance Mode

    Placing a module in maintenance mode means that the Spring Cloud team will no longer be adding new features to the module. +We will fix blocker bugs and security issues, and we will also consider and review small pull requests from the community.

    We intend to continue to support these modules for a period of at least a year from the general availability +of the Greenwich release train.

    The following Spring Cloud Netflix modules and corresponding starters will be placed into maintenance mode:

    • spring-cloud-netflix-archaius
    • spring-cloud-netflix-hystrix-contract
    • spring-cloud-netflix-hystrix-dashboard
    • spring-cloud-netflix-hystrix-stream
    • spring-cloud-netflix-hystrix
    • spring-cloud-netflix-ribbon
    • spring-cloud-netflix-turbine-stream
    • spring-cloud-netflix-turbine
    • spring-cloud-netflix-zuul
    [Note]Note

    This does not include the Eureka or concurrency-limits modules.

    Part IV. Spring Cloud OpenFeign

    Greenwich.SR5

    This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms.

    23. Declarative REST Client: Feign

    Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign.

    23.1 How to Include Feign

    To include Feign in your project use the starter with group org.springframework.cloud +and artifact id spring-cloud-starter-openfeign. See the Spring Cloud Project page +for details on setting up your build system with the current Spring Cloud Release Train.

    Example spring boot app

    @SpringBootApplication
    +@EnableFeignClients
    +public class Application {
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(Application.class, args);
    +    }
    +
    +}

    StoreClient.java.  +

    @FeignClient("stores")
    +public interface StoreClient {
    +    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    +    List<Store> getStores();
    +
    +    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    +    Store update(@PathVariable("storeId") Long storeId, Store store);
    +}

    +

    In the @FeignClient annotation the String value ("stores" above) is +an arbitrary client name, which is used to create a Ribbon load +balancer (see below for details of Ribbon +support). You can also specify a URL using the url attribute +(absolute value or just a hostname). The name of the bean in the +application context is the fully qualified name of the interface. +To specify your own alias value you can use the qualifier value +of the @FeignClient annotation.

    The Ribbon client above will want to discover the physical addresses +for the "stores" service. If your application is a Eureka client then +it will resolve the service in the Eureka service registry. If you +don’t want to use Eureka, you can simply configure a list of servers +in your external configuration (see +above for example).

    23.2 Overriding Feign Defaults

    A central concept in Spring Cloud’s Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the @FeignClient annotation. Spring Cloud creates a new ensemble as an +ApplicationContext on demand for each named client using FeignClientsConfiguration. This contains (amongst other things) an feign.Decoder, a feign.Encoder, and a feign.Contract. +It is possible to override the name of that ensemble by using the contextId +attribute of the @FeignClient annotation.

    Spring Cloud lets you take full control of the feign client by declaring additional configuration (on top of the FeignClientsConfiguration) using @FeignClient. Example:

    @FeignClient(name = "stores", configuration = FooConfiguration.class)
    +public interface StoreClient {
    +    //..
    +}

    In this case the client is composed from the components already in FeignClientsConfiguration together with any in FooConfiguration (where the latter will override the former).

    [Note]Note

    FooConfiguration does not need to be annotated with @Configuration. However, if it is, then take care to exclude it from any @ComponentScan that would otherwise include this configuration as it will become the default source for feign.Decoder, feign.Encoder, feign.Contract, etc., when specified. This can be avoided by putting it in a separate, non-overlapping package from any @ComponentScan or @SpringBootApplication, or it can be explicitly excluded in @ComponentScan.

    [Note]Note

    The serviceId attribute is now deprecated in favor of the name attribute.

    [Note]Note

    Using contextId attribute of the @FeignClient annotation in addition to changing the name of +the ApplicationContext ensemble, it will override the alias of the client name +and it will be used as part of the name of the configuration bean created for that client.

    [Warning]Warning

    Previously, using the url attribute, did not require the name attribute. Using name is now required.

    Placeholders are supported in the name and url attributes.

    @FeignClient(name = "${feign.name}", url = "${feign.url}")
    +public interface StoreClient {
    +    //..
    +}

    Spring Cloud Netflix provides the following beans by default for feign (BeanType beanName: ClassName):

    • Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder)
    • Encoder feignEncoder: SpringEncoder
    • Logger feignLogger: Slf4jLogger
    • Contract feignContract: SpringMvcContract
    • Feign.Builder feignBuilder: HystrixFeign.Builder
    • Client feignClient: if Ribbon is enabled it is a LoadBalancerFeignClient, otherwise the default feign client is used.

    The OkHttpClient and ApacheHttpClient feign clients can be used by setting feign.okhttp.enabled or feign.httpclient.enabled to true, respectively, and having them on the classpath. +You can customize the HTTP client used by providing a bean of either ClosableHttpClient when using Apache or OkHttpClient when using OK HTTP.

    Spring Cloud Netflix does not provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client:

    • Logger.Level
    • Retryer
    • ErrorDecoder
    • Request.Options
    • Collection<RequestInterceptor>
    • SetterFactory

    Creating a bean of one of those type and placing it in a @FeignClient configuration (such as FooConfiguration above) allows you to override each one of the beans described. Example:

    @Configuration
    +public class FooConfiguration {
    +    @Bean
    +    public Contract feignContract() {
    +        return new feign.Contract.Default();
    +    }
    +
    +    @Bean
    +    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
    +        return new BasicAuthRequestInterceptor("user", "password");
    +    }
    +}

    This replaces the SpringMvcContract with feign.Contract.Default and adds a RequestInterceptor to the collection of RequestInterceptor.

    @FeignClient also can be configured using configuration properties.

    application.yml

    feign:
    +  client:
    +    config:
    +      feignName:
    +        connectTimeout: 5000
    +        readTimeout: 5000
    +        loggerLevel: full
    +        errorDecoder: com.example.SimpleErrorDecoder
    +        retryer: com.example.SimpleRetryer
    +        requestInterceptors:
    +          - com.example.FooRequestInterceptor
    +          - com.example.BarRequestInterceptor
    +        decode404: false
    +        encoder: com.example.SimpleEncoder
    +        decoder: com.example.SimpleDecoder
    +        contract: com.example.SimpleContract

    Default configurations can be specified in the @EnableFeignClients attribute defaultConfiguration in a similar manner as described above. The difference is that this configuration will apply to all feign clients.

    If you prefer using configuration properties to configured all @FeignClient, you can create configuration properties with default feign name.

    application.yml

    feign:
    +  client:
    +    config:
    +      default:
    +        connectTimeout: 5000
    +        readTimeout: 5000
    +        loggerLevel: basic

    If we create both @Configuration bean and configuration properties, configuration properties will win. +It will override @Configuration values. But if you want to change the priority to @Configuration, +you can change feign.client.default-to-properties to false.

    [Note]Note

    If you need to use ThreadLocal bound variables in your RequestInterceptor`s you will need to either set the +thread isolation strategy for Hystrix to `SEMAPHORE or disable Hystrix in Feign.

    application.yml

    # To disable Hystrix in Feign
    +feign:
    +  hystrix:
    +    enabled: false
    +
    +# To set thread isolation to SEMAPHORE
    +hystrix:
    +  command:
    +    default:
    +      execution:
    +        isolation:
    +          strategy: SEMAPHORE

    If we want to create multiple feign clients with the same name or url +so that they would point to the same server but each with a different custom configuration then +we have to use contextId attribute of the @FeignClient in order to avoid name +collision of these configuration beans.

    @FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
    +public interface FooClient {
    +    //..
    +}
    @FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
    +public interface BarClient {
    +    //..
    +}

    23.3 Creating Feign Clients Manually

    In some cases it might be necessary to customize your Feign Clients in a way that is not +possible using the methods above. In this case you can create Clients using the +Feign Builder API. Below is an example +which creates two Feign Clients with the same interface but configures each one with +a separate request interceptor.

    @Import(FeignClientsConfiguration.class)
    +class FooController {
    +
    +	private FooClient fooClient;
    +
    +	private FooClient adminClient;
    +
    +    	@Autowired
    +	public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
    +		this.fooClient = Feign.builder().client(client)
    +				.encoder(encoder)
    +				.decoder(decoder)
    +				.contract(contract)
    +				.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
    +				.target(FooClient.class, "http://PROD-SVC");
    +
    +		this.adminClient = Feign.builder().client(client)
    +				.encoder(encoder)
    +				.decoder(decoder)
    +				.contract(contract)
    +				.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
    +				.target(FooClient.class, "http://PROD-SVC");
    +    }
    +}
    [Note]Note

    In the above example FeignClientsConfiguration.class is the default configuration +provided by Spring Cloud Netflix.

    [Note]Note

    PROD-SVC is the name of the service the Clients will be making requests to.

    [Note]Note

    The Feign Contract object defines what annotations and values are valid on interfaces. The +autowired Contract bean provides supports for SpringMVC annotations, instead of +the default Feign native annotations.

    23.4 Feign Hystrix Support

    If Hystrix is on the classpath and feign.hystrix.enabled=true, Feign will wrap all methods with a circuit breaker. Returning a com.netflix.hystrix.HystrixCommand is also available. This lets you use reactive patterns (with a call to .toObservable() or .observe() or asynchronous use (with a call to .queue()).

    To disable Hystrix support on a per-client basis create a vanilla Feign.Builder with the "prototype" scope, e.g.:

    @Configuration
    +public class FooConfiguration {
    +    	@Bean
    +	@Scope("prototype")
    +	public Feign.Builder feignBuilder() {
    +		return Feign.builder();
    +	}
    +}
    [Warning]Warning

    Prior to the Spring Cloud Dalston release, if Hystrix was on the classpath Feign would have wrapped +all methods in a circuit breaker by default. This default behavior was changed in Spring Cloud Dalston in +favor for an opt-in approach.

    23.5 Feign Hystrix Fallbacks

    Hystrix supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error. To enable fallbacks for a given @FeignClient set the fallback attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.

    @FeignClient(name = "hello", fallback = HystrixClientFallback.class)
    +protected interface HystrixClient {
    +    @RequestMapping(method = RequestMethod.GET, value = "/hello")
    +    Hello iFailSometimes();
    +}
    +
    +static class HystrixClientFallback implements HystrixClient {
    +    @Override
    +    public Hello iFailSometimes() {
    +        return new Hello("fallback");
    +    }
    +}

    If one needs access to the cause that made the fallback trigger, one can use the fallbackFactory attribute inside @FeignClient.

    @FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
    +protected interface HystrixClient {
    +	@RequestMapping(method = RequestMethod.GET, value = "/hello")
    +	Hello iFailSometimes();
    +}
    +
    +@Component
    +static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
    +	@Override
    +	public HystrixClient create(Throwable cause) {
    +		return new HystrixClient() {
    +			@Override
    +			public Hello iFailSometimes() {
    +				return new Hello("fallback; reason was: " + cause.getMessage());
    +			}
    +		};
    +	}
    +}
    [Warning]Warning

    There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return com.netflix.hystrix.HystrixCommand and rx.Observable.

    23.6 Feign and @Primary

    When using Feign with Hystrix fallbacks, there are multiple beans in the ApplicationContext of the same type. This will cause @Autowired to not work because there isn’t exactly one bean, or one marked as primary. To work around this, Spring Cloud Netflix marks all Feign instances as @Primary, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the primary attribute of @FeignClient to false.

    @FeignClient(name = "hello", primary = false)
    +public interface HelloClient {
    +	// methods here
    +}

    23.7 Feign Inheritance Support

    Feign supports boilerplate apis via single-inheritance interfaces. +This allows grouping common operations into convenient base interfaces.

    UserService.java.  +

    public interface UserService {
    +
    +    @RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
    +    User getUser(@PathVariable("id") long id);
    +}

    +

    UserResource.java.  +

    @RestController
    +public class UserResource implements UserService {
    +
    +}

    +

    UserClient.java.  +

    package project.user;
    +
    +@FeignClient("users")
    +public interface UserClient extends UserService {
    +
    +}

    +

    [Note]Note

    It is generally not advisable to share an interface between a +server and a client. It introduces tight coupling, and also actually +doesn’t work with Spring MVC in its current form (method parameter +mapping is not inherited).

    23.8 Feign request/response compression

    You may consider enabling the request or response GZIP compression for your +Feign requests. You can do this by enabling one of the properties:

    feign.compression.request.enabled=true
    +feign.compression.response.enabled=true

    Feign request compression gives you settings similar to what you may set for your web server:

    feign.compression.request.enabled=true
    +feign.compression.request.mime-types=text/xml,application/xml,application/json
    +feign.compression.request.min-request-size=2048

    These properties allow you to be selective about the compressed media types and minimum request threshold length.

    23.9 Feign logging

    A logger is created for each Feign client created. By default the name of the logger is the full class name of the interface used to create the Feign client. Feign logging only responds to the DEBUG level.

    application.yml.  +

    logging.level.project.user.UserClient: DEBUG

    +

    The Logger.Level object that you may configure per client, tells Feign how much to log. Choices are:

    • NONE, No logging (DEFAULT).
    • BASIC, Log only the request method and URL and the response status code and execution time.
    • HEADERS, Log the basic information along with request and response headers.
    • FULL, Log the headers, body, and metadata for both requests and responses.

    For example, the following would set the Logger.Level to FULL:

    @Configuration
    +public class FooConfiguration {
    +    @Bean
    +    Logger.Level feignLoggerLevel() {
    +        return Logger.Level.FULL;
    +    }
    +}

    23.10 Feign @QueryMap support

    The OpenFeign @QueryMap annotation provides support for POJOs to be used as +GET parameter maps. Unfortunately, the default OpenFeign QueryMap annotation is +incompatible with Spring because it lacks a value property.

    Spring Cloud OpenFeign provides an equivalent @SpringQueryMap annotation, which +is used to annotate a POJO or Map parameter as a query parameter map.

    For example, the Params class defines parameters param1 and param2:

    // Params.java
    +public class Params {
    +    private String param1;
    +    private String param2;
    +
    +    // [Getters and setters omitted for brevity]
    +}

    The following feign client uses the Params class by using the @SpringQueryMap annotation:

    @FeignClient("demo")
    +public class DemoTemplate {
    +
    +    @GetMapping(path = "/demo")
    +    String demoEndpoint(@SpringQueryMap Params params);
    +}

    23.11 Troubleshooting

    23.11.1 Early Initialization Errors

    Depending on how you are using your Feign clients you may see initialization errors when starting your application. +To work around this problem you can use an ObjectProvider when autowiring your client.

    @Autowired
    +ObjectProvider<TestFeginClient> testFeginClient;

    Part V. Spring Cloud Stream

    24. A Brief History of Spring’s Data Integration Journey

    Spring’s journey on Data Integration started with Spring Integration. With its programming model, it provided a consistent developer experience to build applications that can embrace Enterprise Integration Patterns to connect with external systems such as, databases, message brokers, and among others.

    Fast forward to the cloud-era, where microservices have become prominent in the enterprise setting. Spring Boot transformed the way how developers built Applications. With Spring’s programming model and the runtime responsibilities handled by Spring Boot, it became seamless to develop stand-alone, production-grade Spring-based microservices.

    To extend this to Data Integration workloads, Spring Integration and Spring Boot were put together into a new project. Spring Cloud Stream was born.

    With Spring Cloud Stream, developers can: +* Build, test, iterate, and deploy data-centric applications in isolation. +* Apply modern microservices architecture patterns, including composition through messaging. +* Decouple application responsibilities with event-centric thinking. An event can represent something that has happened in time, to which the downstream consumer applications can react without knowing where it originated or the producer’s identity. +* Port the business logic onto message brokers (such as RabbitMQ, Apache Kafka, Amazon Kinesis). +* Interoperate between channel-based and non-channel-based application binding scenarios to support stateless and stateful computations by using Project Reactor’s Flux and Kafka Streams APIs. +* Rely on the framework’s automatic content-type support for common use-cases. Extending to different data conversion types is possible.

    25. Quick Start

    You can try Spring Cloud Stream in less then 5 min even before you jump into any details by following this three-step guide.

    We show you how to create a Spring Cloud Stream application that receives messages coming from the messaging middleware of your choice (more on this later) and logs received messages to the console. +We call it LoggingConsumer. +While not very practical, it provides a good introduction to some of the main concepts +and abstractions, making it easier to digest the rest of this user guide.

    The three steps are as follows:

    25.1 Creating a Sample Application by Using Spring Initializr

    To get started, visit the Spring Initializr. From there, you can generate our LoggingConsumer application. To do so:

    1. In the Dependencies section, start typing stream. +When the Cloud Stream option should appears, select it.
    2. Start typing either 'kafka' or 'rabbit'.
    3. Select Kafka or RabbitMQ.

      Basically, you choose the messaging middleware to which your application binds. +We recommend using the one you have already installed or feel more comfortable with installing and running. +Also, as you can see from the Initilaizer screen, there are a few other options you can choose. +For example, you can choose Gradle as your build tool instead of Maven (the default).

    4. In the Artifact field, type 'logging-consumer'.

      The value of the Artifact field becomes the application name. +If you chose RabbitMQ for the middleware, your Spring Initializr should now be as follows:

      stream initializr
    5. Click the Generate Project button.

      Doing so downloads the zipped version of the generated project to your hard drive.

    6. Unzip the file into the folder you want to use as your project directory.
    [Tip]Tip

    We encourage you to explore the many possibilities available in the Spring Initializr. +It lets you create many different kinds of Spring applications.

    25.2 Importing the Project into Your IDE

    Now you can import the project into your IDE. +Keep in mind that, depending on the IDE, you may need to follow a specific import procedure. +For example, depending on how the project was generated (Maven or Gradle), you may need to follow specific import procedure (for example, in Eclipse or STS, you need to use File → Import → Maven → Existing Maven Project).

    Once imported, the project must have no errors of any kind. Also, src/main/java should contain com.example.loggingconsumer.LoggingConsumerApplication.

    Technically, at this point, you can run the application’s main class. +It is already a valid Spring Boot application. +However, it does not do anything, so we want to add some code.

    25.3 Adding a Message Handler, Building, and Running

    Modify the com.example.loggingconsumer.LoggingConsumerApplication class to look as follows:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class LoggingConsumerApplication {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(LoggingConsumerApplication.class, args);
    +	}
    +
    +	@StreamListener(Sink.INPUT)
    +	public void handle(Person person) {
    +		System.out.println("Received: " + person);
    +	}
    +
    +	public static class Person {
    +		private String name;
    +		public String getName() {
    +			return name;
    +		}
    +		public void setName(String name) {
    +			this.name = name;
    +		}
    +		public String toString() {
    +			return this.name;
    +		}
    +	}
    +}

    As you can see from the preceding listing:

    • We have enabled Sink binding (input-no-output) by using @EnableBinding(Sink.class). +Doing so signals to the framework to initiate binding to the messaging middleware, where it automatically creates the destination (that is, queue, topic, and others) that are bound to the Sink.INPUT channel.
    • We have added a handler method to receive incoming messages of type Person. +Doing so lets you see one of the core features of the framework: It tries to automatically convert incoming message payloads to type Person.

    You now have a fully functional Spring Cloud Stream application that does listens for messages. +From here, for simplicity, we assume you selected RabbitMQ in step one. +Assuming you have RabbitMQ installed and running, you can start the application by running its main method in your IDE.

    You should see following output:

    	--- [ main] c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg, bound to: input
    +	--- [ main] o.s.a.r.c.CachingConnectionFactory       : Attempting to connect to: [localhost:5672]
    +	--- [ main] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#2a3a299:0/SimpleConnection@66c83fc8. . .
    +	. . .
    +	--- [ main] o.s.i.a.i.AmqpInboundChannelAdapter      : started inbound.input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg
    +	. . .
    +	--- [ main] c.e.l.LoggingConsumerApplication         : Started LoggingConsumerApplication in 2.531 seconds (JVM running for 2.897)

    Go to the RabbitMQ management console or any other RabbitMQ client and send a message to input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg. +The anonymous.CbMIwdkJSBO1ZoPDOtHtCg part represents the group name and is generated, so it is bound to be different in your environment. +For something more predictable, you can use an explicit group name by setting spring.cloud.stream.bindings.input.group=hello (or whatever name you like).

    The contents of the message should be a JSON representation of the Person class, as follows:

    {"name":"Sam Spade"}

    Then, in your console, you should see:

    Received: Sam Spade

    You can also build and package your application into a boot jar (by using ./mvnw clean install) and run the built JAR by using the java -jar command.

    Now you have a working (albeit very basic) Spring Cloud Stream application.

    26. What’s New in 2.0?

    Spring Cloud Stream introduces a number of new features, enhancements, and changes. The following sections outline the most notable ones:

    26.1 New Features and Components

    • Polling Consumers: Introduction of polled consumers, which lets the application control message processing rates. +See Section 29.3.5, “Using Polled Consumers” for more details. +You can also read this blog post for more details.
    • Micrometer Support: Metrics has been switched to use Micrometer. +MeterRegistry is also provided as a bean so that custom applications can autowire it to capture custom metrics. +See Chapter 37, Metrics Emitter for more details.
    • New Actuator Binding Controls: New actuator binding controls let you both visualize and control the Bindings lifecycle. +For more details, see Section 30.6, “Binding visualization and control”.
    • Configurable RetryTemplate: Aside from providing properties to configure RetryTemplate, we now let you provide your own template, effectively overriding the one provided by the framework. +To use it, configure it as a @Bean in your application.

    26.2 Notable Enhancements

    This version includes the following notable enhancements:

    26.2.1 Both Actuator and Web Dependencies Are Now Optional

    This change slims down the footprint of the deployed application in the event neither actuator nor web dependencies required. +It also lets you switch between the reactive and conventional web paradigms by manually adding one of the following dependencies.

    The following listing shows how to add the conventional web framework:

    <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-web</artifactId>
    +</dependency>

    The following listing shows how to add the reactive web framework:

    <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-webflux</artifactId>
    +</dependency>

    The following list shows how to add the actuator dependency:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-actuator</artifactId>
    +</dependency>

    26.2.2 Content-type Negotiation Improvements

    One of the core themes for verion 2.0 is improvements (in both consistency and performance) around content-type negotiation and message conversion. +The following summary outlines the notable changes and improvements in this area. +See the Chapter 32, Content Type Negotiation section for more details. +Also this blog post contains more detail.

    • All message conversion is now handled only by MessageConverter objects.
    • We introduced the @StreamMessageConverter annotation to provide custom MessageConverter objects.
    • We introduced the default Content Type as application/json, which needs to be taken into consideration when migrating 1.3 application or operating in the mixed mode (that is, 1.3 producer → 2.0 consumer).
    • Messages with textual payloads and a contentType of text/…​ or …​/json are no longer converted to Message<String> for cases where the argument type of the provided MessageHandler can not be determined (that is, public void handle(Message<?> message) or public void handle(Object payload)). +Furthermore, a strong argument type may not be enough to properly convert messages, so the contentType header may be used as a supplement by some MessageConverters.

    26.3 Notable Deprecations

    As of version 2.0, the following items have been deprecated:

    26.3.1 Java Serialization (Java Native and Kryo)

    JavaSerializationMessageConverter and KryoMessageConverter remain for now. However, we plan to move them out of the core packages and support in the future. +The main reason for this deprecation is to flag the issue that type-based, language-specific serialization could cause in distributed environments, where Producers and Consumers may depend on different JVM versions or have different versions of supporting libraries (that is, Kryo). +We also wanted to draw the attention to the fact that Consumers and Producers may not even be Java-based, so polyglot style serialization (i.e., JSON) is better suited.

    26.3.2 Deprecated Classes and Methods

    The following is a quick summary of notable deprecations. See the corresponding {spring-cloud-stream-javadoc-current}[javadoc] for more details.

    • SharedChannelRegistry. Use SharedBindingTargetRegistry.
    • Bindings. +Beans qualified by it are already uniquely identified by their type — for example, provided Source, Processor, or custom bindings:
    public interface Sample {
    +	String OUTPUT = "sampleOutput";
    +
    +	@Output(Sample.OUTPUT)
    +	MessageChannel output();
    +}
    • HeaderMode.raw. Use none, headers or embeddedHeaders
    • ProducerProperties.partitionKeyExtractorClass in favor of partitionKeyExtractorName and ProducerProperties.partitionSelectorClass in favor of partitionSelectorName. +This change ensures that both components are Spring configured and managed and are referenced in a Spring-friendly way.
    • BinderAwareRouterBeanPostProcessor. While the component remains, it is no longer a BeanPostProcessor and will be renamed in the future.
    • BinderProperties.setEnvironment(Properties environment). Use BinderProperties.setEnvironment(Map<String, Object> environment).

    This section goes into more detail about how you can work with Spring Cloud Stream. +It covers topics such as creating and running stream applications.

    27. Introducing Spring Cloud Stream

    Spring Cloud Stream is a framework for building message-driven microservice applications. +Spring Cloud Stream builds upon Spring Boot to create standalone, production-grade Spring applications and uses Spring Integration to provide connectivity to message brokers. +It provides opinionated configuration of middleware from several vendors, introducing the concepts of persistent publish-subscribe semantics, consumer groups, and partitions.

    You can add the @EnableBinding annotation to your application to get immediate connectivity to a message broker, and you can add @StreamListener to a method to cause it to receive events for stream processing. +The following example shows a sink application that receives external messages:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class VoteRecordingSinkApplication {
    +
    +  public static void main(String[] args) {
    +    SpringApplication.run(VoteRecordingSinkApplication.class, args);
    +  }
    +
    +  @StreamListener(Sink.INPUT)
    +  public void processVote(Vote vote) {
    +      votingService.recordVote(vote);
    +  }
    +}

    The @EnableBinding annotation takes one or more interfaces as parameters (in this case, the parameter is a single Sink interface). +An interface declares input and output channels. +Spring Cloud Stream provides the Source, Sink, and Processor interfaces. You can also define your own interfaces.

    The following listing shows the definition of the Sink interface:

    public interface Sink {
    +  String INPUT = "input";
    +
    +  @Input(Sink.INPUT)
    +  SubscribableChannel input();
    +}

    The @Input annotation identifies an input channel, through which received messages enter the application. +The @Output annotation identifies an output channel, through which published messages leave the application. +The @Input and @Output annotations can take a channel name as a parameter. +If a name is not provided, the name of the annotated method is used.

    Spring Cloud Stream creates an implementation of the interface for you. +You can use this in the application by autowiring it, as shown in the following example (from a test case):

    @RunWith(SpringJUnit4ClassRunner.class)
    +@SpringApplicationConfiguration(classes = VoteRecordingSinkApplication.class)
    +@WebAppConfiguration
    +@DirtiesContext
    +public class StreamApplicationTests {
    +
    +  @Autowired
    +  private Sink sink;
    +
    +  @Test
    +  public void contextLoads() {
    +    assertNotNull(this.sink.input());
    +  }
    +}

    28. Main Concepts

    Spring Cloud Stream provides a number of abstractions and primitives that simplify the writing of message-driven microservice applications. +This section gives an overview of the following:

    28.1 Application Model

    A Spring Cloud Stream application consists of a middleware-neutral core. +The application communicates with the outside world through input and output channels injected into it by Spring Cloud Stream. +Channels are connected to external brokers through middleware-specific Binder implementations.

    Figure 28.1. Spring Cloud Stream Application

    SCSt with binder

    28.1.1 Fat JAR

    Spring Cloud Stream applications can be run in stand-alone mode from your IDE for testing. +To run a Spring Cloud Stream application in production, you can create an executable (or fat) JAR by using the standard Spring Boot tooling provided for Maven or Gradle. See the Spring Boot Reference Guide for more details.

    28.2 The Binder Abstraction

    Spring Cloud Stream provides Binder implementations for Kafka and Rabbit MQ. +Spring Cloud Stream also includes a TestSupportBinder, which leaves a channel unmodified so that tests can interact with channels directly and reliably assert on what is received. +You can also use the extensible API to write your own Binder.

    Spring Cloud Stream uses Spring Boot for configuration, and the Binder abstraction makes it possible for a Spring Cloud Stream application to be flexible in how it connects to middleware. +For example, deployers can dynamically choose, at runtime, the destinations (such as the Kafka topics or RabbitMQ exchanges) to which channels connect. +Such configuration can be provided through external configuration properties and in any form supported by Spring Boot (including application arguments, environment variables, and application.yml or application.properties files). +In the sink example from the Chapter 27, Introducing Spring Cloud Stream section, setting the spring.cloud.stream.bindings.input.destination application property to raw-sensor-data causes it to read from the raw-sensor-data Kafka topic or from a queue bound to the raw-sensor-data RabbitMQ exchange.

    Spring Cloud Stream automatically detects and uses a binder found on the classpath. +You can use different types of middleware with the same code. +To do so, include a different binder at build time. +For more complex use cases, you can also package multiple binders with your application and have it choose the binder( and even whether to use different binders for different channels) at runtime.

    28.3 Persistent Publish-Subscribe Support

    Communication between applications follows a publish-subscribe model, where data is broadcast through shared topics. +This can be seen in the following figure, which shows a typical deployment for a set of interacting Spring Cloud Stream applications.

    Figure 28.2. Spring Cloud Stream Publish-Subscribe

    SCSt sensors

    Data reported by sensors to an HTTP endpoint is sent to a common destination named raw-sensor-data. +From the destination, it is independently processed by a microservice application that computes time-windowed averages and by another microservice application that ingests the raw data into HDFS (Hadoop Distributed File System). +In order to process the data, both applications declare the topic as their input at runtime.

    The publish-subscribe communication model reduces the complexity of both the producer and the consumer and lets new applications be added to the topology without disruption of the existing flow. +For example, downstream from the average-calculating application, you can add an application that calculates the highest temperature values for display and monitoring. +You can then add another application that interprets the same flow of averages for fault detection. +Doing all communication through shared topics rather than point-to-point queues reduces coupling between microservices.

    While the concept of publish-subscribe messaging is not new, Spring Cloud Stream takes the extra step of making it an opinionated choice for its application model. +By using native middleware support, Spring Cloud Stream also simplifies use of the publish-subscribe model across different platforms.

    28.4 Consumer Groups

    While the publish-subscribe model makes it easy to connect applications through shared topics, the ability to scale up by creating multiple instances of a given application is equally important. +When doing so, different instances of an application are placed in a competing consumer relationship, where only one of the instances is expected to handle a given message.

    Spring Cloud Stream models this behavior through the concept of a consumer group. +(Spring Cloud Stream consumer groups are similar to and inspired by Kafka consumer groups.) +Each consumer binding can use the spring.cloud.stream.bindings.<channelName>.group property to specify a group name. +For the consumers shown in the following figure, this property would be set as spring.cloud.stream.bindings.<channelName>.group=hdfsWrite or spring.cloud.stream.bindings.<channelName>.group=average.

    Figure 28.3. Spring Cloud Stream Consumer Groups

    SCSt groups

    All groups that subscribe to a given destination receive a copy of published data, but only one member of each group receives a given message from that destination. +By default, when a group is not specified, Spring Cloud Stream assigns the application to an anonymous and independent single-member consumer group that is in a publish-subscribe relationship with all other consumer groups.

    28.5 Consumer Types

    Two types of consumer are supported:

    • Message-driven (sometimes referred to as Asynchronous)
    • Polled (sometimes referred to as Synchronous)

    Prior to version 2.0, only asynchronous consumers were supported. A message is delivered as soon as it is available and a thread is available to process it.

    When you wish to control the rate at which messages are processed, you might want to use a synchronous consumer.

    28.5.1 Durability

    Consistent with the opinionated application model of Spring Cloud Stream, consumer group subscriptions are durable. +That is, a binder implementation ensures that group subscriptions are persistent and that, once at least one subscription for a group has been created, the group receives messages, even if they are sent while all applications in the group are stopped.

    [Note]Note

    Anonymous subscriptions are non-durable by nature. +For some binder implementations (such as RabbitMQ), it is possible to have non-durable group subscriptions.

    In general, it is preferable to always specify a consumer group when binding an application to a given destination. +When scaling up a Spring Cloud Stream application, you must specify a consumer group for each of its input bindings. +Doing so prevents the application’s instances from receiving duplicate messages (unless that behavior is desired, which is unusual).

    28.6 Partitioning Support

    Spring Cloud Stream provides support for partitioning data between multiple instances of a given application. +In a partitioned scenario, the physical communication medium (such as the broker topic) is viewed as being structured into multiple partitions. +One or more producer application instances send data to multiple consumer application instances and ensure that data identified by common characteristics are processed by the same consumer instance.

    Spring Cloud Stream provides a common abstraction for implementing partitioned processing use cases in a uniform fashion. +Partitioning can thus be used whether the broker itself is naturally partitioned (for example, Kafka) or not (for example, RabbitMQ).

    Figure 28.4. Spring Cloud Stream Partitioning

    SCSt partitioning

    Partitioning is a critical concept in stateful processing, where it is critical (for either performance or consistency reasons) to ensure that all related data is processed together. +For example, in the time-windowed average calculation example, it is important that all measurements from any given sensor are processed by the same application instance.

    [Note]Note

    To set up a partitioned processing scenario, you must configure both the data-producing and the data-consuming ends.

    29. Programming Model

    To understand the programming model, you should be familiar with the following core concepts:

    • Destination Binders: Components responsible to provide integration with the external messaging systems.
    • Destination Bindings: Bridge between the external messaging systems and application provided Producers and Consumers of messages (created by the Destination Binders).
    • Message: The canonical data structure used by producers and consumers to communicate with Destination Binders (and thus other applications via external messaging systems).
    SCSt overview

    29.1 Destination Binders

    Destination Binders are extension components of Spring Cloud Stream responsible for providing the necessary configuration and implementation to facilitate +integration with external messaging systems. +This integration is responsible for connectivity, delegation, and routing of messages to and from producers and consumers, data type conversion, +invocation of the user code, and more.

    Binders handle a lot of the boiler plate responsibilities that would otherwise fall on your shoulders. However, to accomplish that, the binder still needs +some help in the form of minimalistic yet required set of instructions from the user, which typically come in the form of some type of configuration.

    While it is out of scope of this section to discuss all of the available binder and binding configuration options (the rest of the manual covers them extensively), +Destination Binding does require special attention. The next section discusses it in detail.

    29.2 Destination Bindings

    As stated earlier, Destination Bindings provide a bridge between the external messaging system and application-provided Producers and Consumers.

    Applying the @EnableBinding annotation to one of the application’s configuration classes defines a destination binding. +The @EnableBinding annotation itself is meta-annotated with @Configuration and triggers the configuration of the Spring Cloud Stream infrastructure.

    The following example shows a fully configured and functioning Spring Cloud Stream application that receives the payload of the message from the INPUT +destination as a String type (see Chapter 32, Content Type Negotiation section), logs it to the console and sends it to the OUTPUT destination after converting it to upper case.

    @SpringBootApplication
    +@EnableBinding(Processor.class)
    +public class MyApplication {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(MyApplication.class, args);
    +	}
    +
    +	@StreamListener(Processor.INPUT)
    +	@SendTo(Processor.OUTPUT)
    +	public String handle(String value) {
    +		System.out.println("Received: " + value);
    +		return value.toUpperCase();
    +	}
    +}

    As you can see the @EnableBinding annotation can take one or more interface classes as parameters. The parameters are referred to as bindings, +and they contain methods representing bindable components. +These components are typically message channels (see Spring Messaging) +for channel-based binders (such as Rabbit, Kafka, and others). However other types of bindings can +provide support for the native features of the corresponding technology. For example Kafka Streams binder (formerly known as KStream) allows native bindings directly to Kafka Streams +(see Kafka Streams for more details).

    Spring Cloud Stream already provides binding interfaces for typical message exchange contracts, which include:

    • Sink: Identifies the contract for the message consumer by providing the destination from which the message is consumed.
    • Source: Identifies the contract for the message producer by providing the destination to which the produced message is sent.
    • Processor: Encapsulates both the sink and the source contracts by exposing two destinations that allow consumption and production of messages.
    public interface Sink {
    +
    +  String INPUT = "input";
    +
    +  @Input(Sink.INPUT)
    +  SubscribableChannel input();
    +}
    public interface Source {
    +
    +  String OUTPUT = "output";
    +
    +  @Output(Source.OUTPUT)
    +  MessageChannel output();
    +}
    public interface Processor extends Source, Sink {}

    While the preceding example satisfies the majority of cases, you can also define your own contracts by defining your own bindings interfaces and use @Input and @Output +annotations to identify the actual bindable components.

    For example:

    public interface Barista {
    +
    +    @Input
    +    SubscribableChannel orders();
    +
    +    @Output
    +    MessageChannel hotDrinks();
    +
    +    @Output
    +    MessageChannel coldDrinks();
    +}

    Using the interface shown in the preceding example as a parameter to @EnableBinding triggers the creation of the three bound channels named orders, hotDrinks, and coldDrinks, +respectively.

    You can provide as many binding interfaces as you need, as arguments to the @EnableBinding annotation, as shown in the following example:

    @EnableBinding(value = { Orders.class, Payment.class })

    In Spring Cloud Stream, the bindable MessageChannel components are the Spring Messaging MessageChannel (for outbound) and its extension, SubscribableChannel, +(for inbound).

    Pollable Destination Binding

    While the previously described bindings support event-based message consumption, sometimes you need more control, such as rate of consumption.

    Starting with version 2.0, you can now bind a pollable consumer:

    The following example shows how to bind a pollable consumer:

    public interface PolledBarista {
    +
    +    @Input
    +    PollableMessageSource orders();
    +	. . .
    +}

    In this case, an implementation of PollableMessageSource is bound to the orders “channel”. See Section 29.3.5, “Using Polled Consumers” for more details.

    Customizing Channel Names

    By using the @Input and @Output annotations, you can specify a customized channel name for the channel, as shown in the following example:

    public interface Barista {
    +    @Input("inboundOrders")
    +    SubscribableChannel orders();
    +}

    In the preceding example, the created bound channel is named inboundOrders.

    Normally, you need not access individual channels or bindings directly (other then configuring them via @EnableBinding annotation). However there may be +times, such as testing or other corner cases, when you do.

    Aside from generating channels for each binding and registering them as Spring beans, for each bound interface, Spring Cloud Stream generates a bean that implements the interface. +That means you can have access to the interfaces representing the bindings or individual channels by auto-wiring either in your application, as shown in the following two examples:

    Autowire Binding interface

    @Autowire
    +private Source source
    +
    +public void sayHello(String name) {
    +    source.output().send(MessageBuilder.withPayload(name).build());
    +}

    Autowire individual channel

    @Autowire
    +private MessageChannel output;
    +
    +public void sayHello(String name) {
    +    output.send(MessageBuilder.withPayload(name).build());
    +}

    You can also use standard Spring’s @Qualifier annotation for cases when channel names are customized or in multiple-channel scenarios that require specifically named channels.

    The following example shows how to use the @Qualifier annotation in this way:

    @Autowire
    +@Qualifier("myChannel")
    +private MessageChannel output;

    29.3 Producing and Consuming Messages

    You can write a Spring Cloud Stream application by using either Spring Integration annotations or Spring Cloud Stream native annotation.

    29.3.1 Spring Integration Support

    Spring Cloud Stream is built on the concepts and patterns defined by Enterprise Integration Patterns and relies +in its internal implementation on an already established and popular implementation of Enterprise Integration Patterns within the Spring portfolio of projects: +Spring Integration framework.

    So its only natural for it to support the foundation, semantics, and configuration options that are already established by Spring Integration

    For example, you can attach the output channel of a Source to a MessageSource and use the familiar @InboundChannelAdapter annotation, as follows:

    @EnableBinding(Source.class)
    +public class TimerSource {
    +
    +  @Bean
    +  @InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedDelay = "10", maxMessagesPerPoll = "1"))
    +  public MessageSource<String> timerMessageSource() {
    +    return () -> new GenericMessage<>("Hello Spring Cloud Stream");
    +  }
    +}

    Similarly, you can use @Transformer or @ServiceActivator while providing an implementation of a message handler method for a Processor binding contract, as shown in the following example:

    @EnableBinding(Processor.class)
    +public class TransformProcessor {
    +  @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
    +  public Object transform(String message) {
    +    return message.toUpperCase();
    +  }
    +}
    [Note]Note

    While this may be skipping ahead a bit, it is important to understand that, when you consume from the same binding using @StreamListener annotation, a pub-sub model is used. +Each method annotated with @StreamListener receives its own copy of a message, and each one has its own consumer group. +However, if you consume from the same binding by using one of the Spring Integration annotation (such as @Aggregator, @Transformer, or @ServiceActivator), those consume in a competing model. +No individual consumer group is created for each subscription.

    29.3.2 Using @StreamListener Annotation

    Complementary to its Spring Integration support, Spring Cloud Stream provides its own @StreamListener annotation, modeled after other Spring Messaging annotations +(@MessageMapping, @JmsListener, @RabbitListener, and others) and provides conviniences, such as content-based routing and others.

    @EnableBinding(Sink.class)
    +public class VoteHandler {
    +
    +  @Autowired
    +  VotingService votingService;
    +
    +  @StreamListener(Sink.INPUT)
    +  public void handle(Vote vote) {
    +    votingService.record(vote);
    +  }
    +}

    As with other Spring Messaging methods, method arguments can be annotated with @Payload, @Headers, and @Header.

    For methods that return data, you must use the @SendTo annotation to specify the output binding destination for data returned by the method, as shown in the following example:

    @EnableBinding(Processor.class)
    +public class TransformProcessor {
    +
    +  @Autowired
    +  VotingService votingService;
    +
    +  @StreamListener(Processor.INPUT)
    +  @SendTo(Processor.OUTPUT)
    +  public VoteResult handle(Vote vote) {
    +    return votingService.record(vote);
    +  }
    +}

    29.3.3 Using @StreamListener for Content-based routing

    Spring Cloud Stream supports dispatching messages to multiple handler methods annotated with @StreamListener based on conditions.

    In order to be eligible to support conditional dispatching, a method must satisfy the follow conditions:

    • It must not return a value.
    • It must be an individual message handling method (reactive API methods are not supported).

    The condition is specified by a SpEL expression in the condition argument of the annotation and is evaluated for each message. +All the handlers that match the condition are invoked in the same thread, and no assumption must be made about the order in which the invocations take place.

    In the following example of a @StreamListener with dispatching conditions, all the messages bearing a header type with the value bogey are dispatched to the +receiveBogey method, and all the messages bearing a header type with the value bacall are dispatched to the receiveBacall method.

    @EnableBinding(Sink.class)
    +@EnableAutoConfiguration
    +public static class TestPojoWithAnnotatedArguments {
    +
    +    @StreamListener(target = Sink.INPUT, condition = "headers['type']=='bogey'")
    +    public void receiveBogey(@Payload BogeyPojo bogeyPojo) {
    +       // handle the message
    +    }
    +
    +    @StreamListener(target = Sink.INPUT, condition = "headers['type']=='bacall'")
    +    public void receiveBacall(@Payload BacallPojo bacallPojo) {
    +       // handle the message
    +    }
    +}

    Content Type Negotiation in the Context of condition

    It is important to understand some of the mechanics behind content-based routing using the condition argument of @StreamListener, especially in the context of the type of the message as a whole. +It may also help if you familiarize yourself with the Chapter 32, Content Type Negotiation before you proceed.

    Consider the following scenario:

    @EnableBinding(Sink.class)
    +@EnableAutoConfiguration
    +public static class CatsAndDogs {
    +
    +    @StreamListener(target = Sink.INPUT, condition = "payload.class.simpleName=='Dog'")
    +    public void bark(Dog dog) {
    +       // handle the message
    +    }
    +
    +    @StreamListener(target = Sink.INPUT, condition = "payload.class.simpleName=='Cat'")
    +    public void purr(Cat cat) {
    +       // handle the message
    +    }
    +}

    The preceding code is perfectly valid. It compiles and deploys without any issues, yet it never produces the result you expect.

    That is because you are testing something that does not yet exist in a state you expect. That is because the payload of the message is not yet converted from the +wire format (byte[]) to the desired type. +In other words, it has not yet gone through the type conversion process described in the Chapter 32, Content Type Negotiation.

    So, unless you use a SPeL expression that evaluates raw data (for example, the value of the first byte in the byte array), use message header-based expressions +(such as condition = "headers['type']=='dog'").

    [Note]Note

    At the moment, dispatching through @StreamListener conditions is supported only for channel-based binders (not for reactive programming) +support.

    29.3.4 Spring Cloud Function support

    Since Spring Cloud Stream v2.1, another alternative for defining stream handlers and sources is to use build-in +support for Spring Cloud Function where they can be expressed as beans of + type java.util.function.[Supplier/Function/Consumer].

    To specify which functional bean to bind to the external destination(s) exposed by the bindings, you must provide spring.cloud.stream.function.definition property.

    Here is the example of the Processor application exposing message handler as java.util.function.Function

    @SpringBootApplication
    +@EnableBinding(Processor.class)
    +public class MyFunctionBootApp {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(MyFunctionBootApp.class, "--spring.cloud.stream.function.definition=toUpperCase");
    +	}
    +
    +	@Bean
    +	public Function<String, String> toUpperCase() {
    +		return s -> s.toUpperCase();
    +	}
    +}

    In the above you we simply define a bean of type java.util.function.Function called toUpperCase and identify it as a bean to be used as message handler +whose 'input' and 'output' must be bound to the external destinations exposed by the Processor binding.

    Below are the examples of simple functional applications to support Source, Processor and Sink.

    Here is the example of a Source application defined as java.util.function.Supplier

    @SpringBootApplication
    +@EnableBinding(Source.class)
    +public static class SourceFromSupplier {
    +	public static void main(String[] args) {
    +		SpringApplication.run(SourceFromSupplier.class, "--spring.cloud.stream.function.definition=date");
    +	}
    +	@Bean
    +	public Supplier<Date> date() {
    +		return () -> new Date(12345L);
    +	}
    +}

    Here is the example of a Processor application defined as java.util.function.Function

    @SpringBootApplication
    +@EnableBinding(Processor.class)
    +public static class ProcessorFromFunction {
    +	public static void main(String[] args) {
    +		SpringApplication.run(ProcessorFromFunction.class, "--spring.cloud.stream.function.definition=toUpperCase");
    +	}
    +	@Bean
    +	public Function<String, String> toUpperCase() {
    +		return s -> s.toUpperCase();
    +	}
    +}

    Here is the example of a Sink application defined as java.util.function.Consumer

    @EnableAutoConfiguration
    +@EnableBinding(Sink.class)
    +public static class SinkFromConsumer {
    +	public static void main(String[] args) {
    +		SpringApplication.run(SinkFromConsumer.class, "--spring.cloud.stream.function.definition=sink");
    +	}
    +	@Bean
    +	public Consumer<String> sink() {
    +		return System.out::println;
    +	}
    +}

    Functional Composition

    Using this programming model you can also benefit from functional composition where you can dynamically compose complex handlers from a set of simple functions. +As an example let’s add the following function bean to the application defined above

    @Bean
    +public Function<String, String> wrapInQuotes() {
    +	return s -> "\"" + s + "\"";
    +}

    and modify the spring.cloud.stream.function.definition property to reflect your intention to compose a new function from both ‘toUpperCase’ and ‘wrapInQuotes’. +To do that Spring Cloud Function allows you to use | (pipe) symbol. So to finish our example our property will now look like this:

    —spring.cloud.stream.function.definition=toUpperCase|wrapInQuotes

    29.3.5 Using Polled Consumers

    Overview

    When using polled consumers, you poll the PollableMessageSource on demand. +Consider the following example of a polled consumer:

    public interface PolledConsumer {
    +
    +    @Input
    +    PollableMessageSource destIn();
    +
    +    @Output
    +    MessageChannel destOut();
    +
    +}

    Given the polled consumer in the preceding example, you might use it as follows:

    @Bean
    +public ApplicationRunner poller(PollableMessageSource destIn, MessageChannel destOut) {
    +    return args -> {
    +        while (someCondition()) {
    +            try {
    +                if (!destIn.poll(m -> {
    +                    String newPayload = ((String) m.getPayload()).toUpperCase();
    +                    destOut.send(new GenericMessage<>(newPayload));
    +                })) {
    +                    Thread.sleep(1000);
    +                }
    +            }
    +            catch (Exception e) {
    +                // handle failure
    +            }
    +        }
    +    };
    +}

    The PollableMessageSource.poll() method takes a MessageHandler argument (often a lambda expression, as shown here). +It returns true if the message was received and successfully processed.

    As with message-driven consumers, if the MessageHandler throws an exception, messages are published to error channels, as discussed in ???.

    Normally, the poll() method acknowledges the message when the MessageHandler exits. +If the method exits abnormally, the message is rejected (not re-queued), but see the section called “Handling Errors”. +You can override that behavior by taking responsibility for the acknowledgment, as shown in the following example:

    @Bean
    +public ApplicationRunner poller(PollableMessageSource dest1In, MessageChannel dest2Out) {
    +    return args -> {
    +        while (someCondition()) {
    +            if (!dest1In.poll(m -> {
    +                StaticMessageHeaderAccessor.getAcknowledgmentCallback(m).noAutoAck();
    +                // e.g. hand off to another thread which can perform the ack
    +                // or acknowledge(Status.REQUEUE)
    +
    +            })) {
    +                Thread.sleep(1000);
    +            }
    +        }
    +    };
    +}
    [Important]Important

    You must ack (or nack) the message at some point, to avoid resource leaks.

    [Important]Important

    Some messaging systems (such as Apache Kafka) maintain a simple offset in a log. If a delivery fails and is re-queued with StaticMessageHeaderAccessor.getAcknowledgmentCallback(m).acknowledge(Status.REQUEUE);, any later successfully ack’d messages are redelivered.

    There is also an overloaded poll method, for which the definition is as follows:

    poll(MessageHandler handler, ParameterizedTypeReference<?> type)

    The type is a conversion hint that allows the incoming message payload to be converted, as shown in the following example:

    boolean result = pollableSource.poll(received -> {
    +			Map<String, Foo> payload = (Map<String, Foo>) received.getPayload();
    +            ...
    +
    +		}, new ParameterizedTypeReference<Map<String, Foo>>() {});

    Handling Errors

    By default, an error channel is configured for the pollable source; if the callback throws an exception, an ErrorMessage is sent to the error channel (<destination>.<group>.errors); this error channel is also bridged to the global Spring Integration errorChannel.

    You can subscribe to either error channel with a @ServiceActivator to handle errors; without a subscription, the error will simply be logged and the message will be acknowledged as successful. +If the error channel service activator throws an exception, the message will be rejected (by default) and won’t be redelivered. +If the service activator throws a RequeueCurrentMessageException, the message will be requeued at the broker and will be again retrieved on a subsequent poll.

    If the listener throws a RequeueCurrentMessageException directly, the message will be requeued, as discussed above, and will not be sent to the error channels.

    29.4 Error Handling

    Errors happen, and Spring Cloud Stream provides several flexible mechanisms to handle them. +The error handling comes in two flavors:

    • application: The error handling is done within the application (custom error handler).
    • system: The error handling is delegated to the binder (re-queue, DL, and others). Note that the techniques are dependent on binder implementation and the +capability of the underlying messaging middleware.

    Spring Cloud Stream uses the Spring Retry library to facilitate successful message processing. See Section 29.4.3, “Retry Template” for more details. +However, when all fails, the exceptions thrown by the message handlers are propagated back to the binder. At that point, binder invokes custom error handler or communicates +the error back to the messaging system (re-queue, DLQ, and others).

    29.4.1 Application Error Handling

    There are two types of application-level error handling. Errors can be handled at each binding subscription or a global handler can handle all the binding subscription errors. Let’s review the details.

    Figure 29.1. A Spring Cloud Stream Sink Application with Custom and Global Error Handlers

    custom vs global error channels

    For each input binding, Spring Cloud Stream creates a dedicated error channel with the following semantics <destinationName>.errors.

    [Note]Note

    The <destinationName> consists of the name of the binding (such as input) and the name of the group (such as myGroup).

    Consider the following:

    spring.cloud.stream.bindings.input.group=myGroup
    @StreamListener(Sink.INPUT) // destination name 'input.myGroup'
    +public void handle(Person value) {
    +	throw new RuntimeException("BOOM!");
    +}
    +
    +@ServiceActivator(inputChannel = Processor.INPUT + ".myGroup.errors") //channel name 'input.myGroup.errors'
    +public void error(Message<?> message) {
    +	System.out.println("Handling ERROR: " + message);
    +}

    In the preceding example the destination name is input.myGroup and the dedicated error channel name is input.myGroup.errors.

    [Note]Note

    The use of @StreamListener annotation is intended specifically to define bindings that bridge internal channels and external destinations. Given that the destination +specific error channel does NOT have an associated external destination, such channel is a prerogative of Spring Integration (SI). This means that the handler +for such destination must be defined using one of the SI handler annotations (i.e., @ServiceActivator, @Transformer etc.).

    [Note]Note

    If group is not specified anonymous group is used (something like input.anonymous.2K37rb06Q6m2r51-SPIDDQ), which is not suitable for error +handling scenarious, since you don’t know what it’s going to be until the destination is created.

    Also, in the event you are binding to the existing destination such as:

    spring.cloud.stream.bindings.input.destination=myFooDestination
    +spring.cloud.stream.bindings.input.group=myGroup

    the full destination name is myFooDestination.myGroup and then the dedicated error channel name is myFooDestination.myGroup.errors.

    Back to the example…​

    The handle(..) method, which subscribes to the channel named input, throws an exception. Given there is also a subscriber to the error channel input.myGroup.errors +all error messages are handled by this subscriber.

    If you have multiple bindings, you may want to have a single error handler. Spring Cloud Stream automatically provides support for +a global error channel by bridging each individual error channel to the channel named errorChannel, allowing a single subscriber to handle all errors, +as shown in the following example:

    @StreamListener("errorChannel")
    +public void error(Message<?> message) {
    +	System.out.println("Handling ERROR: " + message);
    +}

    This may be a convenient option if error handling logic is the same regardless of which handler produced the error.

    29.4.2 System Error Handling

    System-level error handling implies that the errors are communicated back to the messaging system and, given that not every messaging system +is the same, the capabilities may differ from binder to binder.

    That said, in this section we explain the general idea behind system level error handling and use Rabbit binder as an example. NOTE: Kafka binder provides similar +support, although some configuration properties do differ. Also, for more details and configuration options, see the individual binder’s documentation.

    If no internal error handlers are configured, the errors propagate to the binders, and the binders subsequently propagate those errors back to the messaging system. +Depending on the capabilities of the messaging system such a system may drop the message, re-queue the message for re-processing or send the failed message to DLQ. +Both Rabbit and Kafka support these concepts. However, other binders may not, so refer to your individual binder’s documentation for details on supported system-level +error-handling options.

    Drop Failed Messages

    By default, if no additional system-level configuration is provided, the messaging system drops the failed message. +While acceptable in some cases, for most cases, it is not, and we need some recovery mechanism to avoid message loss.

    DLQ - Dead Letter Queue

    DLQ allows failed messages to be sent to a special destination: - Dead Letter Queue.

    When configured, failed messages are sent to this destination for subsequent re-processing or auditing and reconciliation.

    For example, continuing on the previous example and to set up the DLQ with Rabbit binder, you need to set the following property:

    spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true

    Keep in mind that, in the above property, input corresponds to the name of the input destination binding. +The consumer indicates that it is a consumer property and auto-bind-dlq instructs the binder to configure DLQ for input +destination, which results in an additional Rabbit queue named input.myGroup.dlq.

    Once configured, all failed messages are routed to this queue with an error message similar to the following:

    delivery_mode:	1
    +headers:
    +x-death:
    +count:	1
    +reason:	rejected
    +queue:	input.hello
    +time:	1522328151
    +exchange:
    +routing-keys:	input.myGroup
    +Payload {"name”:"Bob"}

    As you can see from the above, your original message is preserved for further actions.

    However, one thing you may have noticed is that there is limited information on the original issue with the message processing. For example, you do not see a stack +trace corresponding to the original error. +To get more relevant information about the original error, you must set an additional property:

    spring.cloud.stream.rabbit.bindings.input.consumer.republish-to-dlq=true

    Doing so forces the internal error handler to intercept the error message and add additional information to it before publishing it to DLQ. +Once configured, you can see that the error message contains more information relevant to the original error, as follows:

    delivery_mode:	2
    +headers:
    +x-original-exchange:
    +x-exception-message:	has an error
    +x-original-routingKey:	input.myGroup
    +x-exception-stacktrace:	org.springframework.messaging.MessageHandlingException: nested exception is
    +      org.springframework.messaging.MessagingException: has an error, failedMessage=GenericMessage [payload=byte[15],
    +      headers={amqp_receivedDeliveryMode=NON_PERSISTENT, amqp_receivedRoutingKey=input.hello, amqp_deliveryTag=1,
    +      deliveryAttempt=3, amqp_consumerQueue=input.hello, amqp_redelivered=false, id=a15231e6-3f80-677b-5ad7-d4b1e61e486e,
    +      amqp_consumerTag=amq.ctag-skBFapilvtZhDsn0k3ZmQg, contentType=application/json, timestamp=1522327846136}]
    +      at org.spring...integ...han...MethodInvokingMessageProcessor.processMessage(MethodInvokingMessageProcessor.java:107)
    +      at. . . . .
    +Payload {"name”:"Bob"}

    This effectively combines application-level and system-level error handling to further assist with downstream troubleshooting mechanics.

    Re-queue Failed Messages

    As mentioned earlier, the currently supported binders (Rabbit and Kafka) rely on RetryTemplate to facilitate successful message processing. See Section 29.4.3, “Retry Template” for details. +However, for cases when max-attempts property is set to 1, internal reprocessing of the message is disabled. At this point, you can facilitate message re-processing (re-tries) +by instructing the messaging system to re-queue the failed message. Once re-queued, the failed message is sent back to the original handler, essentially creating a retry loop.

    This option may be feasible for cases where the nature of the error is related to some sporadic yet short-term unavailability of some resource.

    To accomplish that, you must set the following properties:

    spring.cloud.stream.bindings.input.consumer.max-attempts=1
    +spring.cloud.stream.rabbit.bindings.input.consumer.requeue-rejected=true

    In the preceding example, the max-attempts set to 1 essentially disabling internal re-tries and requeue-rejected (short for requeue rejected messages) is set to true. +Once set, the failed message is resubmitted to the same handler and loops continuously or until the handler throws AmqpRejectAndDontRequeueException +essentially allowing you to build your own re-try logic within the handler itself.

    29.4.3 Retry Template

    The RetryTemplate is part of the Spring Retry library. +While it is out of scope of this document to cover all of the capabilities of the RetryTemplate, we will mention the following consumer properties that are specifically related to +the RetryTemplate:

    maxAttempts

    The number of attempts to process the message.

    Default: 3.

    backOffInitialInterval

    The backoff initial interval on retry.

    Default 1000 milliseconds.

    backOffMaxInterval

    The maximum backoff interval.

    Default 10000 milliseconds.

    backOffMultiplier

    The backoff multiplier.

    Default 2.0.

    defaultRetryable

    Whether exceptions thrown by the listener that are not listed in the retryableExceptions are retryable.

    Default: true.

    retryableExceptions

    A map of Throwable class names in the key and a boolean in the value. +Specify those exceptions (and subclasses) that will or won’t be retried. +Also see defaultRetriable. +Example: spring.cloud.stream.bindings.input.consumer.retryable-exceptions.java.lang.IllegalStateException=false.

    Default: empty.

    While the preceding settings are sufficient for majority of the customization requirements, they may not satisfy certain complex requirements at, which +point you may want to provide your own instance of the RetryTemplate. To do so configure it as a bean in your application configuration. The application provided +instance will override the one provided by the framework. Also, to avoid conflicts you must qualify the instance of the RetryTemplate you want to be used by the binder +as @StreamRetryTemplate. For example,

    @StreamRetryTemplate
    +public RetryTemplate myRetryTemplate() {
    +    return new RetryTemplate();
    +}

    As you can see from the above example you don’t need to annotate it with @Bean since @StreamRetryTemplate is a qualified @Bean.

    29.5 Reactive Programming Support

    Spring Cloud Stream also supports the use of reactive APIs where incoming and outgoing data is handled as continuous data flows. +Support for reactive APIs is available through spring-cloud-stream-reactive, which needs to be added explicitly to your project.

    The programming model with reactive APIs is declarative. Instead of specifying how each individual message should be handled, you can use operators that describe functional transformations from inbound to outbound data flows.

    At present Spring Cloud Stream supports the only the Reactor API. +In the future, we intend to support a more generic model based on Reactive Streams.

    The reactive programming model also uses the @StreamListener annotation for setting up reactive handlers. +The differences are that:

    • The @StreamListener annotation must not specify an input or output, as they are provided as arguments and return values from the method.
    • The arguments of the method must be annotated with @Input and @Output, indicating which input or output the incoming and outgoing data flows connect to, respectively.
    • The return value of the method, if any, is annotated with @Output, indicating the input where data should be sent.
    [Note]Note

    Reactive programming support requires Java 1.8.

    [Note]Note

    As of Spring Cloud Stream 1.1.1 and later (starting with release train Brooklyn.SR2), reactive programming support requires the use of Reactor 3.0.4.RELEASE and higher. +Earlier Reactor versions (including 3.0.1.RELEASE, 3.0.2.RELEASE and 3.0.3.RELEASE) are not supported. +spring-cloud-stream-reactive transitively retrieves the proper version, but it is possible for the project structure to manage the version of the io.projectreactor:reactor-core to an earlier release, especially when using Maven. +This is the case for projects generated by using Spring Initializr with Spring Boot 1.x, which overrides the Reactor version to 2.0.8.RELEASE. +In such cases, you must ensure that the proper version of the artifact is released. +You can do so by adding a direct dependency on io.projectreactor:reactor-core with a version of 3.0.4.RELEASE or later to your project.

    [Note]Note

    The use of term, reactive, currently refers to the reactive APIs being used and not to the execution model being reactive (that is, the bound endpoints still use a 'push' rather than a 'pull' model). While some backpressure support is provided by the use of Reactor, we do intend, in a future release, to support entirely reactive pipelines by the use of native reactive clients for the connected middleware.

    29.5.1 Reactor-based Handlers

    A Reactor-based handler can have the following argument types:

    • For arguments annotated with @Input, it supports the Reactor Flux type. +The parameterization of the inbound Flux follows the same rules as in the case of individual message handling: It can be the entire Message, a POJO that can be the Message payload, or a POJO that is the result of a transformation based on the Message content-type header. Multiple inputs are provided.
    • For arguments annotated with Output, it supports the FluxSender type, which connects a Flux produced by the method with an output. Generally speaking, specifying outputs as arguments is only recommended when the method can have multiple outputs.

    A Reactor-based handler supports a return type of Flux. In that case, it must be annotated with @Output. We recommend using the return value of the method when a single output Flux is available.

    The following example shows a Reactor-based Processor:

    @EnableBinding(Processor.class)
    +@EnableAutoConfiguration
    +public static class UppercaseTransformer {
    +
    +  @StreamListener
    +  @Output(Processor.OUTPUT)
    +  public Flux<String> receive(@Input(Processor.INPUT) Flux<String> input) {
    +    return input.map(s -> s.toUpperCase());
    +  }
    +}

    The same processor using output arguments looks like the following example:

    @EnableBinding(Processor.class)
    +@EnableAutoConfiguration
    +public static class UppercaseTransformer {
    +
    +  @StreamListener
    +  public void receive(@Input(Processor.INPUT) Flux<String> input,
    +     @Output(Processor.OUTPUT) FluxSender output) {
    +     output.send(input.map(s -> s.toUpperCase()));
    +  }
    +}

    29.5.2 Reactive Sources

    Spring Cloud Stream reactive support also provides the ability for creating reactive sources through the @StreamEmitter annotation. +By using the @StreamEmitter annotation, a regular source may be converted to a reactive one. +@StreamEmitter is a method level annotation that marks a method to be an emitter to outputs declared with @EnableBinding. +You cannot use the @Input annotation along with @StreamEmitter, as the methods marked with this annotation are not listening for any input. Rather, methods marked with @StreamEmitter generate output. +Following the same programming model used in @StreamListener, @StreamEmitter also allows flexible ways of using the @Output annotation, depending on whether the method has any arguments, a return type, and other considerations.

    The remainder of this section contains examples of using the @StreamEmitter annotation in various styles.

    The following example emits the Hello, World message every millisecond and publishes to a Reactor Flux:

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  @Output(Source.OUTPUT)
    +  public Flux<String> emit() {
    +    return Flux.intervalMillis(1)
    +            .map(l -> "Hello World");
    +  }
    +}

    In the preceding example, the resulting messages in the Flux are sent to the output channel of the Source.

    The next example is another flavor of an @StreamEmmitter that sends a Reactor Flux. +Instead of returning a Flux, the following method uses a FluxSender to programmatically send a Flux from a source:

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  @Output(Source.OUTPUT)
    +  public void emit(FluxSender output) {
    +    output.send(Flux.intervalMillis(1)
    +            .map(l -> "Hello World"));
    +  }
    +}

    The next example is exactly same as the above snippet in functionality and style. +However, instead of using an explicit @Output annotation on the method, it uses the annotation on the method parameter.

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  public void emit(@Output(Source.OUTPUT) FluxSender output) {
    +    output.send(Flux.intervalMillis(1)
    +            .map(l -> "Hello World"));
    +  }
    +}

    The last example in this section is yet another flavor of writing reacting sources by using the Reactive Streams Publisher API and taking advantage of the support for it in Spring Integration Java DSL. +The Publisher in the following example still uses Reactor Flux under the hood, but, from an application perspective, that is transparent to the user and only needs Reactive Streams and Java DSL for Spring Integration:

    @EnableBinding(Source.class)
    +@EnableAutoConfiguration
    +public static class HelloWorldEmitter {
    +
    +  @StreamEmitter
    +  @Output(Source.OUTPUT)
    +  @Bean
    +  public Publisher<Message<String>> emit() {
    +    return IntegrationFlows.from(() ->
    +                new GenericMessage<>("Hello World"),
    +        e -> e.poller(p -> p.fixedDelay(1)))
    +        .toReactivePublisher();
    +  }
    +}

    30. Binders

    Spring Cloud Stream provides a Binder abstraction for use in connecting to physical destinations at the external middleware. +This section provides information about the main concepts behind the Binder SPI, its main components, and implementation-specific details.

    30.1 Producers and Consumers

    The following image shows the general relationship of producers and consumers:

    Figure 30.1. Producers and Consumers

    producers consumers

    A producer is any component that sends messages to a channel. +The channel can be bound to an external message broker with a Binder implementation for that broker. +When invoking the bindProducer() method, the first parameter is the name of the destination within the broker, the second parameter is the local channel instance to which the producer sends messages, and the third parameter contains properties (such as a partition key expression) to be used within the adapter that is created for that channel.

    A consumer is any component that receives messages from a channel. +As with a producer, the consumer’s channel can be bound to an external message broker. +When invoking the bindConsumer() method, the first parameter is the destination name, and a second parameter provides the name of a logical group of consumers. +Each group that is represented by consumer bindings for a given destination receives a copy of each message that a producer sends to that destination (that is, it follows normal publish-subscribe semantics). +If there are multiple consumer instances bound with the same group name, then messages are load-balanced across those consumer instances so that each message sent by a producer is consumed by only a single consumer instance within each group (that is, it follows normal queueing semantics).

    30.2 Binder SPI

    The Binder SPI consists of a number of interfaces, out-of-the box utility classes, and discovery strategies that provide a pluggable mechanism for connecting to external middleware.

    The key point of the SPI is the Binder interface, which is a strategy for connecting inputs and outputs to external middleware. The following listing shows the definnition of the Binder interface:

    public interface Binder<T, C extends ConsumerProperties, P extends ProducerProperties> {
    +    Binding<T> bindConsumer(String name, String group, T inboundBindTarget, C consumerProperties);
    +
    +    Binding<T> bindProducer(String name, T outboundBindTarget, P producerProperties);
    +}

    The interface is parameterized, offering a number of extension points:

    • Input and output bind targets. As of version 1.0, only MessageChannel is supported, but this is intended to be used as an extension point in the future.
    • Extended consumer and producer properties, allowing specific Binder implementations to add supplemental properties that can be supported in a type-safe manner.

    A typical binder implementation consists of the following:

    • A class that implements the Binder interface;
    • A Spring @Configuration class that creates a bean of type Binder along with the middleware connection infrastructure.
    • A META-INF/spring.binders file found on the classpath containing one or more binder definitions, as shown in the following example:

      kafka:\
      +org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration

    30.3 Binder Detection

    Spring Cloud Stream relies on implementations of the Binder SPI to perform the task of connecting channels to message brokers. +Each Binder implementation typically connects to one type of messaging system.

    30.3.1 Classpath Detection

    By default, Spring Cloud Stream relies on Spring Boot’s auto-configuration to configure the binding process. +If a single Binder implementation is found on the classpath, Spring Cloud Stream automatically uses it. +For example, a Spring Cloud Stream project that aims to bind only to RabbitMQ can add the following dependency:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    +</dependency>

    For the specific Maven coordinates of other binder dependencies, see the documentation of that binder implementation.

    30.4 Multiple Binders on the Classpath

    When multiple binders are present on the classpath, the application must indicate which binder is to be used for each channel binding. +Each binder configuration contains a META-INF/spring.binders file, which is a simple properties file, as shown in the following example:

    rabbit:\
    +org.springframework.cloud.stream.binder.rabbit.config.RabbitServiceAutoConfiguration

    Similar files exist for the other provided binder implementations (such as Kafka), and custom binder implementations are expected to provide them as well. +The key represents an identifying name for the binder implementation, whereas the value is a comma-separated list of configuration classes that each contain one and only one bean definition of type org.springframework.cloud.stream.binder.Binder.

    Binder selection can either be performed globally, using the spring.cloud.stream.defaultBinder property (for example, spring.cloud.stream.defaultBinder=rabbit) or individually, by configuring the binder on each channel binding. +For instance, a processor application (that has channels named input and output for read and write respectively) that reads from Kafka and writes to RabbitMQ can specify the following configuration:

    spring.cloud.stream.bindings.input.binder=kafka
    +spring.cloud.stream.bindings.output.binder=rabbit

    30.5 Connecting to Multiple Systems

    By default, binders share the application’s Spring Boot auto-configuration, so that one instance of each binder found on the classpath is created. +If your application should connect to more than one broker of the same type, you can specify multiple binder configurations, each with different environment settings.

    [Note]Note

    Turning on explicit binder configuration disables the default binder configuration process altogether. +If you do so, all binders in use must be included in the configuration. +Frameworks that intend to use Spring Cloud Stream transparently may create binder configurations that can be referenced by name, but they do not affect the default binder configuration. +In order to do so, a binder configuration may have its defaultCandidate flag set to false (for example, spring.cloud.stream.binders.<configurationName>.defaultCandidate=false). +This denotes a configuration that exists independently of the default binder configuration process.

    The following example shows a typical configuration for a processor application that connects to two RabbitMQ broker instances:

    spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        input:
    +          destination: thing1
    +          binder: rabbit1
    +        output:
    +          destination: thing2
    +          binder: rabbit2
    +      binders:
    +        rabbit1:
    +          type: rabbit
    +          environment:
    +            spring:
    +              rabbitmq:
    +                host: <host1>
    +        rabbit2:
    +          type: rabbit
    +          environment:
    +            spring:
    +              rabbitmq:
    +                host: <host2>

    30.6 Binding visualization and control

    Since version 2.0, Spring Cloud Stream supports visualization and control of the Bindings through Actuator endpoints.

    Starting with version 2.0 actuator and web are optional, you must first add one of the web dependencies as well as add the actuator dependency manually. +The following example shows how to add the dependency for the Web framework:

    <dependency>
    +     <groupId>org.springframework.boot</groupId>
    +     <artifactId>spring-boot-starter-web</artifactId>
    +</dependency>

    The following example shows how to add the dependency for the WebFlux framework:

    <dependency>
    +       <groupId>org.springframework.boot</groupId>
    +       <artifactId>spring-boot-starter-webflux</artifactId>
    +</dependency>

    You can add the Actuator dependency as follows:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-actuator</artifactId>
    +</dependency>
    [Note]Note

    To run Spring Cloud Stream 2.0 apps in Cloud Foundry, you must add spring-boot-starter-web and spring-boot-starter-actuator to the classpath. Otherwise, the +application will not start due to health check failures.

    You must also enable the bindings actuator endpoints by setting the following property: --management.endpoints.web.exposure.include=bindings.

    Once those prerequisites are satisfied. you should see the following in the logs when application start:

    : Mapped "{[/actuator/bindings/{name}],methods=[POST]. . .
    +: Mapped "{[/actuator/bindings],methods=[GET]. . .
    +: Mapped "{[/actuator/bindings/{name}],methods=[GET]. . .

    To visualize the current bindings, access the following URL: +http://<host>:<port>/actuator/bindings

    Alternative, to see a single binding, access one of the URLs similar to the following: +http://<host>:<port>/actuator/bindings/myBindingName

    You can also stop, start, pause, and resume individual bindings by posting to the same URL while providing a state argument as JSON, as shown in the following examples:

    curl -d '{"state":"STOPPED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"STARTED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"PAUSED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"RESUMED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName

    [Note]Note

    PAUSED and RESUMED work only when the corresponding binder and its underlying technology supports it. Otherwise, you see the warning message in the logs. +Currently, only Kafka binder supports the PAUSED and RESUMED states.

    30.7 Binder Configuration Properties

    The following properties are available when customizing binder configurations. These properties exposed via org.springframework.cloud.stream.config.BinderProperties

    They must be prefixed with spring.cloud.stream.binders.<configurationName>.

    type

    The binder type. +It typically references one of the binders found on the classpath — in particular, a key in a META-INF/spring.binders file.

    By default, it has the same value as the configuration name.

    inheritEnvironment

    Whether the configuration inherits the environment of the application itself.

    Default: true.

    environment

    Root for a set of properties that can be used to customize the environment of the binder. +When this property is set, the context in which the binder is being created is not a child of the application context. +This setting allows for complete separation between the binder components and the application components.

    Default: empty.

    defaultCandidate

    Whether the binder configuration is a candidate for being considered a default binder or can be used only when explicitly referenced. +This setting allows adding binder configurations without interfering with the default processing.

    Default: true.

    31. Configuration Options

    Spring Cloud Stream supports general configuration options as well as configuration for bindings and binders. +Some binders let additional binding properties support middleware-specific features.

    Configuration options can be provided to Spring Cloud Stream applications through any mechanism supported by Spring Boot. +This includes application arguments, environment variables, and YAML or .properties files.

    31.1 Binding Service Properties

    These properties are exposed via org.springframework.cloud.stream.config.BindingServiceProperties

    spring.cloud.stream.instanceCount

    The number of deployed instances of an application. +Must be set for partitioning on the producer side. Must be set on the consumer side when using RabbitMQ and with Kafka if autoRebalanceEnabled=false.

    Default: 1.

    spring.cloud.stream.instanceIndex
    The instance index of the application: A number from 0 to instanceCount - 1. +Used for partitioning with RabbitMQ and with Kafka if autoRebalanceEnabled=false. +Automatically set in Cloud Foundry to match the application’s instance index.
    spring.cloud.stream.dynamicDestinations

    A list of destinations that can be bound dynamically (for example, in a dynamic routing scenario). +If set, only listed destinations can be bound.

    Default: empty (letting any destination be bound).

    spring.cloud.stream.defaultBinder

    The default binder to use, if multiple binders are configured. +See Multiple Binders on the Classpath.

    Default: empty.

    spring.cloud.stream.overrideCloudConnectors

    This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. +If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). +When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). +The typical usage of this property is to be nested in a customized environment when connecting to multiple systems.

    Default: false.

    spring.cloud.stream.bindingRetryInterval

    The interval (in seconds) between retrying binding creation when, for example, the binder does not support late binding and the broker (for example, Apache Kafka) is down. +Set it to zero to treat such conditions as fatal, preventing the application from starting.

    Default: 30

    31.2 Binding Properties

    Binding properties are supplied by using the format of spring.cloud.stream.bindings.<channelName>.<property>=<value>. +The <channelName> represents the name of the channel being configured (for example, output for a Source).

    To avoid repetition, Spring Cloud Stream supports setting values for all channels, in the format of spring.cloud.stream.default.<property>=<value>.

    When it comes to avoiding repetitions for extended binding properties, this format should be used - spring.cloud.stream.<binder-type>.default.<producer|consumer>.<property>=<value>.

    In what follows, we indicate where we have omitted the spring.cloud.stream.bindings.<channelName>. prefix and focus just on the property name, with the understanding that the prefix ise included at runtime.

    31.2.1 Common Binding Properties

    These properties are exposed via org.springframework.cloud.stream.config.BindingProperties

    The following binding properties are available for both input and output bindings and must be prefixed with spring.cloud.stream.bindings.<channelName>. (for example, spring.cloud.stream.bindings.input.destination=ticktock).

    Default values can be set by using the spring.cloud.stream.default prefix (for example`spring.cloud.stream.default.contentType=application/json`).

    destination
    The target destination of a channel on the bound middleware (for example, the RabbitMQ exchange or Kafka topic). +If the channel is bound as a consumer, it could be bound to multiple destinations, and the destination names can be specified as comma-separated String values. +If not set, the channel name is used instead. +The default value of this property cannot be overridden.
    group

    The consumer group of the channel. +Applies only to inbound bindings. +See Consumer Groups.

    Default: null (indicating an anonymous consumer).

    contentType

    The content type of the channel. +See Chapter 32, Content Type Negotiation.

    Default: application/json.

    binder

    The binder used by this binding. +See Section 30.4, “Multiple Binders on the Classpath” for details.

    Default: null (the default binder is used, if it exists).

    31.2.2 Consumer Properties

    These properties are exposed via org.springframework.cloud.stream.binder.ConsumerProperties

    The following binding properties are available for input bindings only and must be prefixed with spring.cloud.stream.bindings.<channelName>.consumer. (for example, spring.cloud.stream.bindings.input.consumer.concurrency=3).

    Default values can be set by using the spring.cloud.stream.default.consumer prefix (for example, spring.cloud.stream.default.consumer.headerMode=none).

    concurrency

    The concurrency of the inbound consumer.

    Default: 1.

    partitioned

    Whether the consumer receives data from a partitioned producer.

    Default: false.

    headerMode

    When set to none, disables header parsing on input. +Effective only for messaging middleware that does not support message headers natively and requires header embedding. +This option is useful when consuming data from non-Spring Cloud Stream applications when native headers are not supported. +When set to headers, it uses the middleware’s native header mechanism. +When set to embeddedHeaders, it embeds headers into the message payload.

    Default: depends on the binder implementation.

    maxAttempts

    If processing fails, the number of attempts to process the message (including the first). +Set to 1 to disable retry.

    Default: 3.

    backOffInitialInterval

    The backoff initial interval on retry.

    Default: 1000.

    backOffMaxInterval

    The maximum backoff interval.

    Default: 10000.

    backOffMultiplier

    The backoff multiplier.

    Default: 2.0.

    defaultRetryable

    Whether exceptions thrown by the listener that are not listed in the retryableExceptions are retryable.

    Default: true.

    instanceIndex

    When set to a value greater than equal to zero, it allows customizing the instance index of this consumer (if different from spring.cloud.stream.instanceIndex). +When set to a negative value, it defaults to spring.cloud.stream.instanceIndex. +See Section 34.2, “Instance Index and Instance Count” for more information.

    Default: -1.

    instanceCount

    When set to a value greater than equal to zero, it allows customizing the instance count of this consumer (if different from spring.cloud.stream.instanceCount). +When set to a negative value, it defaults to spring.cloud.stream.instanceCount. +See Section 34.2, “Instance Index and Instance Count” for more information.

    Default: -1.

    retryableExceptions

    A map of Throwable class names in the key and a boolean in the value. +Specify those exceptions (and subclasses) that will or won’t be retried. +Also see defaultRetriable. +Example: spring.cloud.stream.bindings.input.consumer.retryable-exceptions.java.lang.IllegalStateException=false.

    Default: empty.

    useNativeDecoding

    When set to true, the inbound message is deserialized directly by the client library, which must be configured correspondingly (for example, setting an appropriate Kafka producer value deserializer). +When this configuration is being used, the inbound message unmarshalling is not based on the contentType of the binding. +When native decoding is used, it is the responsibility of the producer to use an appropriate encoder (for example, the Kafka producer value serializer) to serialize the outbound message. +Also, when native encoding and decoding is used, the headerMode=embeddedHeaders property is ignored and headers are not embedded in the message. +See the producer property useNativeEncoding.

    Default: false.

    31.2.3 Producer Properties

    These properties are exposed via org.springframework.cloud.stream.binder.ProducerProperties

    The following binding properties are available for output bindings only and must be prefixed with spring.cloud.stream.bindings.<channelName>.producer. (for example, spring.cloud.stream.bindings.input.producer.partitionKeyExpression=payload.id).

    Default values can be set by using the prefix spring.cloud.stream.default.producer (for example, spring.cloud.stream.default.producer.partitionKeyExpression=payload.id).

    partitionKeyExpression

    A SpEL expression that determines how to partition outbound data. +If set, or if partitionKeyExtractorClass is set, outbound data on this channel is partitioned. partitionCount must be set to a value greater than 1 to be effective. +Mutually exclusive with partitionKeyExtractorClass. +See Section 28.6, “Partitioning Support”.

    Default: null.

    partitionKeyExtractorClass

    A PartitionKeyExtractorStrategy implementation. +If set, or if partitionKeyExpression is set, outbound data on this channel is partitioned. partitionCount must be set to a value greater than 1 to be effective. +Mutually exclusive with partitionKeyExpression. +See Section 28.6, “Partitioning Support”.

    Default: null.

    partitionSelectorClass

    A PartitionSelectorStrategy implementation. +Mutually exclusive with partitionSelectorExpression. +If neither is set, the partition is selected as the hashCode(key) % partitionCount, where key is computed through either partitionKeyExpression or partitionKeyExtractorClass.

    Default: null.

    partitionSelectorExpression

    A SpEL expression for customizing partition selection. +Mutually exclusive with partitionSelectorClass. +If neither is set, the partition is selected as the hashCode(key) % partitionCount, where key is computed through either partitionKeyExpression or partitionKeyExtractorClass.

    Default: null.

    partitionCount

    The number of target partitions for the data, if partitioning is enabled. +Must be set to a value greater than 1 if the producer is partitioned. +On Kafka, it is interpreted as a hint. The larger of this and the partition count of the target topic is used instead.

    Default: 1.

    requiredGroups
    A comma-separated list of groups to which the producer must ensure message delivery even if they start after it has been created (for example, by pre-creating durable queues in RabbitMQ).
    headerMode

    When set to none, it disables header embedding on output. +It is effective only for messaging middleware that does not support message headers natively and requires header embedding. +This option is useful when producing data for non-Spring Cloud Stream applications when native headers are not supported. +When set to headers, it uses the middleware’s native header mechanism. +When set to embeddedHeaders, it embeds headers into the message payload.

    Default: Depends on the binder implementation.

    useNativeEncoding

    When set to true, the outbound message is serialized directly by the client library, which must be configured correspondingly (for example, setting an appropriate Kafka producer value serializer). +When this configuration is being used, the outbound message marshalling is not based on the contentType of the binding. +When native encoding is used, it is the responsibility of the consumer to use an appropriate decoder (for example, the Kafka consumer value de-serializer) to deserialize the inbound message. +Also, when native encoding and decoding is used, the headerMode=embeddedHeaders property is ignored and headers are not embedded in the message. +See the consumer property useNativeDecoding.

    Default: false.

    errorChannelEnabled

    When set to true, if the binder supports asynchroous send results, send failures are sent to an error channel for the destination. +See ??? for more information.

    Default: false.

    31.3 Using Dynamically Bound Destinations

    Besides the channels defined by using @EnableBinding, Spring Cloud Stream lets applications send messages to dynamically bound destinations. +This is useful, for example, when the target destination needs to be determined at runtime. +Applications can do so by using the BinderAwareChannelResolver bean, registered automatically by the @EnableBinding annotation.

    The 'spring.cloud.stream.dynamicDestinations' property can be used for restricting the dynamic destination names to a known set (whitelisting). +If this property is not set, any destination can be bound dynamically.

    The BinderAwareChannelResolver can be used directly, as shown in the following example of a REST controller using a path variable to decide the target channel:

    @EnableBinding
    +@Controller
    +public class SourceWithDynamicDestination {
    +
    +    @Autowired
    +    private BinderAwareChannelResolver resolver;
    +
    +    @RequestMapping(path = "/{target}", method = POST, consumes = "*/*")
    +    @ResponseStatus(HttpStatus.ACCEPTED)
    +    public void handleRequest(@RequestBody String body, @PathVariable("target") target,
    +           @RequestHeader(HttpHeaders.CONTENT_TYPE) Object contentType) {
    +        sendMessage(body, target, contentType);
    +    }
    +
    +    private void sendMessage(String body, String target, Object contentType) {
    +        resolver.resolveDestination(target).send(MessageBuilder.createMessage(body,
    +                new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType))));
    +    }
    +}

    Now consider what happens when we start the application on the default port (8080) and make the following requests with CURL:

    curl -H "Content-Type: application/json" -X POST -d "customer-1" http://localhost:8080/customers
    +
    +curl -H "Content-Type: application/json" -X POST -d "order-1" http://localhost:8080/orders

    The destinations, 'customers' and 'orders', are created in the broker (in the exchange for Rabbit or in the topic for Kafka) with names of 'customers' and 'orders', and the data is published to the appropriate destinations.

    The BinderAwareChannelResolver is a general-purpose Spring Integration DestinationResolver and can be injected in other components — for example, in a router using a SpEL expression based on the target field of an incoming JSON message. The following example includes a router that reads SpEL expressions:

    @EnableBinding
    +@Controller
    +public class SourceWithDynamicDestination {
    +
    +    @Autowired
    +    private BinderAwareChannelResolver resolver;
    +
    +
    +    @RequestMapping(path = "/", method = POST, consumes = "application/json")
    +    @ResponseStatus(HttpStatus.ACCEPTED)
    +    public void handleRequest(@RequestBody String body, @RequestHeader(HttpHeaders.CONTENT_TYPE) Object contentType) {
    +        sendMessage(body, contentType);
    +    }
    +
    +    private void sendMessage(Object body, Object contentType) {
    +        routerChannel().send(MessageBuilder.createMessage(body,
    +                new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType))));
    +    }
    +
    +    @Bean(name = "routerChannel")
    +    public MessageChannel routerChannel() {
    +        return new DirectChannel();
    +    }
    +
    +    @Bean
    +    @ServiceActivator(inputChannel = "routerChannel")
    +    public ExpressionEvaluatingRouter router() {
    +        ExpressionEvaluatingRouter router =
    +            new ExpressionEvaluatingRouter(new SpelExpressionParser().parseExpression("payload.target"));
    +        router.setDefaultOutputChannelName("default-output");
    +        router.setChannelResolver(resolver);
    +        return router;
    +    }
    +}

    The Router Sink Application uses this technique to create the destinations on-demand.

    If the channel names are known in advance, you can configure the producer properties as with any other destination. +Alternatively, if you register a NewBindingCallback<> bean, it is invoked just before the binding is created. +The callback takes the generic type of the extended producer properties used by the binder. +It has one method:

    void configure(String channelName, MessageChannel channel, ProducerProperties producerProperties,
    +        T extendedProducerProperties);

    The following example shows how to use the RabbitMQ binder:

    @Bean
    +public NewBindingCallback<RabbitProducerProperties> dynamicConfigurer() {
    +    return (name, channel, props, extended) -> {
    +        props.setRequiredGroups("bindThisQueue");
    +        extended.setQueueNameGroupOnly(true);
    +        extended.setAutoBindDlq(true);
    +        extended.setDeadLetterQueueName("myDLQ");
    +    };
    +}
    [Note]Note

    If you need to support dynamic destinations with multiple binder types, use Object for the generic type and cast the extended argument as needed.

    32. Content Type Negotiation

    Data transformation is one of the core features of any message-driven microservice architecture. Given that, in Spring Cloud Stream, such data +is represented as a Spring Message, a message may have to be transformed to a desired shape or size before reaching its destination. This is required for two reasons:

    1. To convert the contents of the incoming message to match the signature of the application-provided handler.
    2. To convert the contents of the outgoing message to the wire format.

    The wire format is typically byte[] (that is true for the Kafka and Rabbit binders), but it is governed by the binder implementation.

    In Spring Cloud Stream, message transformation is accomplished with an org.springframework.messaging.converter.MessageConverter.

    [Note]Note

    As a supplement to the details to follow, you may also want to read the following blog post.

    32.1 Mechanics

    To better understand the mechanics and the necessity behind content-type negotiation, we take a look at a very simple use case by using the following message handler as an example:

    @StreamListener(Processor.INPUT)
    +@SendTo(Processor.OUTPUT)
    +public String handle(Person person) {..}
    [Note]Note

    For simplicity, we assume that this is the only handler in the application (we assume there is no internal pipeline).

    The handler shown in the preceding example expects a Person object as an argument and produces a String type as an output. +In order for the framework to succeed in passing the incoming Message as an argument to this handler, it has to somehow transform the payload of the Message type from the wire format to a Person type. +In other words, the framework must locate and apply the appropriate MessageConverter. +To accomplish that, the framework needs some instructions from the user. +One of these instructions is already provided by the signature of the handler method itself (Person type). +Consequently, in theory, that should be (and, in some cases, is) enough. +However, for the majority of use cases, in order to select the appropriate MessageConverter, the framework needs an additional piece of information. +That missing piece is contentType.

    Spring Cloud Stream provides three mechanisms to define contentType (in order of precedence):

    1. HEADER: The contentType can be communicated through the Message itself. By providing a contentType header, you declare the content type to use to locate and apply the appropriate MessageConverter.
    2. BINDING: The contentType can be set per destination binding by setting the spring.cloud.stream.bindings.input.content-type property.

      [Note]Note

      The input segment in the property name corresponds to the actual name of the destination (which is “input” in our case). This approach lets you declare, on a per-binding basis, the content type to use to locate and apply the appropriate MessageConverter.

    3. DEFAULT: If contentType is not present in the Message header or the binding, the default application/json content type is used to +locate and apply the appropriate MessageConverter.

    As mentioned earlier, the preceding list also demonstrates the order of precedence in case of a tie. For example, a header-provided content type takes precedence over any other content type. +The same applies for a content type set on a per-binding basis, which essentially lets you override the default content type. +However, it also provides a sensible default (which was determined from community feedback).

    Another reason for making application/json the default stems from the interoperability requirements driven by distributed microservices architectures, where producer and consumer not only run in different JVMs but can also run on different non-JVM platforms.

    When the non-void handler method returns, if the the return value is already a Message, that Message becomes the payload. However, when the return value is not a Message, the new Message is constructed with the return value as the payload while inheriting +headers from the input Message minus the headers defined or filtered by SpringIntegrationProperties.messageHandlerNotPropagatedHeaders. +By default, there is only one header set there: contentType. This means that the new Message does not have contentType header set, thus ensuring that the contentType can evolve. +You can always opt out of returning a Message from the handler method where you can inject any header you wish.

    If there is an internal pipeline, the Message is sent to the next handler by going through the same process of conversion. However, if there is no internal pipeline or you have reached the end of it, the Message is sent back to the output destination.

    32.1.1 Content Type versus Argument Type

    As mentioned earlier, for the framework to select the appropriate MessageConverter, it requires argument type and, optionally, content type information. +The logic for selecting the appropriate MessageConverter resides with the argument resolvers (HandlerMethodArgumentResolvers), which trigger right before the invocation of the user-defined handler method (which is when the actual argument type is known to the framework). +If the argument type does not match the type of the current payload, the framework delegates to the stack of the +pre-configured MessageConverters to see if any one of them can convert the payload. +As you can see, the Object fromMessage(Message<?> message, Class<?> targetClass); +operation of the MessageConverter takes targetClass as one of its arguments. +The framework also ensures that the provided Message always contains a contentType header. +When no contentType header was already present, it injects either the per-binding contentType header or the default contentType header. +The combination of contentType argument type is the mechanism by which framework determines if message can be converted to a target type. +If no appropriate MessageConverter is found, an exception is thrown, which you can handle by adding a custom MessageConverter (see Section 32.3, “User-defined Message Converters”).

    But what if the payload type matches the target type declared by the handler method? In this case, there is nothing to convert, and the +payload is passed unmodified. While this sounds pretty straightforward and logical, keep in mind handler methods that take a Message<?> or Object as an argument. +By declaring the target type to be Object (which is an instanceof everything in Java), you essentially forfeit the conversion process.

    [Note]Note

    Do not expect Message to be converted into some other type based only on the contentType. +Remember that the contentType is complementary to the target type. +If you wish, you can provide a hint, which MessageConverter may or may not take into consideration.

    32.1.2 Message Converters

    MessageConverters define two methods:

    Object fromMessage(Message<?> message, Class<?> targetClass);
    +
    +Message<?> toMessage(Object payload, @Nullable MessageHeaders headers);

    It is important to understand the contract of these methods and their usage, specifically in the context of Spring Cloud Stream.

    The fromMessage method converts an incoming Message to an argument type. +The payload of the Message could be any type, and it is +up to the actual implementation of the MessageConverter to support multiple types. +For example, some JSON converter may support the payload type as byte[], String, and others. +This is important when the application contains an internal pipeline (that is, input → handler1 → handler2 →. . . → output) and the output of the upstream handler results in a Message which may not be in the initial wire format.

    However, the toMessage method has a more strict contract and must always convert Message to the wire format: byte[].

    So, for all intents and purposes (and especially when implementing your own converter) you regard the two methods as having the following signatures:

    Object fromMessage(Message<?> message, Class<?> targetClass);
    +
    +Message<byte[]> toMessage(Object payload, @Nullable MessageHeaders headers);

    32.2 Provided MessageConverters

    As mentioned earlier, the framework already provides a stack of MessageConverters to handle most common use cases. +The following list describes the provided MessageConverters, in order of precedence (the first MessageConverter that works is used):

    1. ApplicationJsonMessageMarshallingConverter: Variation of the org.springframework.messaging.converter.MappingJackson2MessageConverter. Supports conversion of the payload of the Message to/from POJO for cases when contentType is application/json (DEFAULT).
    2. TupleJsonMessageConverter: DEPRECATED Supports conversion of the payload of the Message to/from org.springframework.tuple.Tuple.
    3. ByteArrayMessageConverter: Supports conversion of the payload of the Message from byte[] to byte[] for cases when contentType is application/octet-stream. It is essentially a pass through and exists primarily for backward compatibility.
    4. ObjectStringMessageConverter: Supports conversion of any type to a String when contentType is text/plain. +It invokes Object’s toString() method or, if the payload is byte[], a new String(byte[]).
    5. JavaSerializationMessageConverter: DEPRECATED Supports conversion based on java serialization when contentType is application/x-java-serialized-object.
    6. KryoMessageConverter: DEPRECATED Supports conversion based on Kryo serialization when contentType is application/x-java-object.
    7. JsonUnmarshallingConverter: Similar to the ApplicationJsonMessageMarshallingConverter. It supports conversion of any type when contentType is application/x-java-object. +It expects the actual type information to be embedded in the contentType as an attribute (for example, application/x-java-object;type=foo.bar.Cat).

    When no appropriate converter is found, the framework throws an exception. When that happens, you should check your code and configuration and ensure you did not miss anything (that is, ensure that you provided a contentType by using a binding or a header). +However, most likely, you found some uncommon case (such as a custom contentType perhaps) and the current stack of provided MessageConverters +does not know how to convert. If that is the case, you can add custom MessageConverter. See Section 32.3, “User-defined Message Converters”.

    32.3 User-defined Message Converters

    Spring Cloud Stream exposes a mechanism to define and register additional MessageConverters. +To use it, implement org.springframework.messaging.converter.MessageConverter, configure it as a @Bean, and annotate it with @StreamMessageConverter. +It is then apended to the existing stack of `MessageConverter`s.

    [Note]Note

    It is important to understand that custom MessageConverter implementations are added to the head of the existing stack. +Consequently, custom MessageConverter implementations take precedence over the existing ones, which lets you override as well as add to the existing converters.

    The following example shows how to create a message converter bean to support a new content type called application/bar:

    @EnableBinding(Sink.class)
    +@SpringBootApplication
    +public static class SinkApplication {
    +
    +    ...
    +
    +    @Bean
    +    @StreamMessageConverter
    +    public MessageConverter customMessageConverter() {
    +        return new MyCustomMessageConverter();
    +    }
    +}
    +
    +public class MyCustomMessageConverter extends AbstractMessageConverter {
    +
    +    public MyCustomMessageConverter() {
    +        super(new MimeType("application", "bar"));
    +    }
    +
    +    @Override
    +    protected boolean supports(Class<?> clazz) {
    +        return (Bar.class.equals(clazz));
    +    }
    +
    +    @Override
    +    protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
    +        Object payload = message.getPayload();
    +        return (payload instanceof Bar ? payload : new Bar((byte[]) payload));
    +    }
    +}

    Spring Cloud Stream also provides support for Avro-based converters and schema evolution. +See Chapter 33, Schema Evolution Support for details.

    33. Schema Evolution Support

    Spring Cloud Stream provides support for schema evolution so that the data can be evolved over time and still work with older or newer producers and consumers and vice versa. +Most serialization models, especially the ones that aim for portability across different platforms and languages, rely on a schema that describes how the data is serialized in the binary payload. +In order to serialize the data and then to interpret it, both the sending and receiving sides must have access to a schema that describes the binary format. +In certain cases, the schema can be inferred from the payload type on serialization or from the target type on deserialization. +However, many applications benefit from having access to an explicit schema that describes the binary data format. +A schema registry lets you store schema information in a textual format (typically JSON) and makes that information accessible to various applications that need it to receive and send data in binary format. +A schema is referenceable as a tuple consisting of:

    • A subject that is the logical name of the schema
    • The schema version
    • The schema format, which describes the binary format of the data

    This following sections goes through the details of various components involved in schema evolution process.

    33.1 Schema Registry Client

    The client-side abstraction for interacting with schema registry servers is the SchemaRegistryClient interface, which has the following structure:

    public interface SchemaRegistryClient {
    +
    +    SchemaRegistrationResponse register(String subject, String format, String schema);
    +
    +    String fetch(SchemaReference schemaReference);
    +
    +    String fetch(Integer id);
    +
    +}

    Spring Cloud Stream provides out-of-the-box implementations for interacting with its own schema server and for interacting with the Confluent Schema Registry.

    A client for the Spring Cloud Stream schema registry can be configured by using the @EnableSchemaRegistryClient, as follows:

      @EnableBinding(Sink.class)
    +  @SpringBootApplication
    +  @EnableSchemaRegistryClient
    +  public static class AvroSinkApplication {
    +    ...
    +  }
    [Note]Note

    The default converter is optimized to cache not only the schemas from the remote server but also the parse() and toString() methods, which are quite expensive. +Because of this, it uses a DefaultSchemaRegistryClient that does not cache responses. +If you intend to change the default behavior, you can use the client directly on your code and override it to the desired outcome. +To do so, you have to add the property spring.cloud.stream.schemaRegistryClient.cached=true to your application properties.

    33.1.1 Schema Registry Client Properties

    The Schema Registry Client supports the following properties:

    spring.cloud.stream.schemaRegistryClient.endpoint
    The location of the schema-server. +When setting this, use a full URL, including protocol (http or https) , port, and context path.
    Default
    http://localhost:8990/
    spring.cloud.stream.schemaRegistryClient.cached
    Whether the client should cache schema server responses. +Normally set to false, as the caching happens in the message converter. +Clients using the schema registry client should set this to true.
    Default
    true

    33.2 Avro Schema Registry Client Message Converters

    For applications that have a SchemaRegistryClient bean registered with the application context, Spring Cloud Stream auto configures an Apache Avro message converter for schema management. +This eases schema evolution, as applications that receive messages can get easy access to a writer schema that can be reconciled with their own reader schema.

    For outbound messages, if the content type of the channel is set to application/*+avro, the MessageConverter is activated, as shown in the following example:

    spring.cloud.stream.bindings.output.contentType=application/*+avro

    During the outbound conversion, the message converter tries to infer the schema of each outbound messages (based on its type) and register it to a subject (based on the payload type) by using the SchemaRegistryClient. +If an identical schema is already found, then a reference to it is retrieved. +If not, the schema is registered, and a new version number is provided. +The message is sent with a contentType header by using the following scheme: application/[prefix].[subject].v[version]+avro, where prefix is configurable and subject is deduced from the payload type.

    For example, a message of the type User might be sent as a binary payload with a content type of application/vnd.user.v2+avro, where user is the subject and 2 is the version number.

    When receiving messages, the converter infers the schema reference from the header of the incoming message and tries to retrieve it. The schema is used as the writer schema in the deserialization process.

    33.2.1 Avro Schema Registry Message Converter Properties

    If you have enabled Avro based schema registry client by setting spring.cloud.stream.bindings.output.contentType=application/*+avro, you can customize the behavior of the registration by setting the following properties.

    spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled

    Enable if you want the converter to use reflection to infer a Schema from a POJO.

    Default: false

    spring.cloud.stream.schema.avro.readerSchema
    Avro compares schema versions by looking at a writer schema (origin payload) and a reader schema (your application payload). See the Avro documentation for more information. If set, this overrides any lookups at the schema server and uses the local schema as the reader schema. +Default: null
    spring.cloud.stream.schema.avro.schemaLocations

    Registers any .avsc files listed in this property with the Schema Server.

    Default: empty

    spring.cloud.stream.schema.avro.prefix

    The prefix to be used on the Content-Type header.

    Default: vnd

    33.3 Apache Avro Message Converters

    Spring Cloud Stream provides support for schema-based message converters through its spring-cloud-stream-schema module. +Currently, the only serialization format supported out of the box for schema-based message converters is Apache Avro, with more formats to be added in future versions.

    The spring-cloud-stream-schema module contains two types of message converters that can be used for Apache Avro serialization:

    • Converters that use the class information of the serialized or deserialized objects or a schema with a location known at startup.
    • Converters that use a schema registry. They locate the schemas at runtime and dynamically register new schemas as domain objects evolve.

    33.4 Converters with Schema Support

    The AvroSchemaMessageConverter supports serializing and deserializing messages either by using a predefined schema or by using the schema information available in the class (either reflectively or contained in the SpecificRecord). +If you provide a custom converter, then the default AvroSchemaMessageConverter bean is not created. The following example shows a custom converter:

    To use custom converters, you can simply add it to the application context, optionally specifying one or more MimeTypes with which to associate it. +The default MimeType is application/avro.

    If the target type of the conversion is a GenericRecord, a schema must be set.

    The following example shows how to configure a converter in a sink application by registering the Apache Avro MessageConverter without a predefined schema. +In this example, note that the mime type value is avro/bytes, not the default application/avro.

    @EnableBinding(Sink.class)
    +@SpringBootApplication
    +public static class SinkApplication {
    +
    +  ...
    +
    +  @Bean
    +  public MessageConverter userMessageConverter() {
    +      return new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes"));
    +  }
    +}

    Conversely, the following application registers a converter with a predefined schema (found on the classpath):

    @EnableBinding(Sink.class)
    +@SpringBootApplication
    +public static class SinkApplication {
    +
    +  ...
    +
    +  @Bean
    +  public MessageConverter userMessageConverter() {
    +      AvroSchemaMessageConverter converter = new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes"));
    +      converter.setSchemaLocation(new ClassPathResource("schemas/User.avro"));
    +      return converter;
    +  }
    +}

    33.5 Schema Registry Server

    Spring Cloud Stream provides a schema registry server implementation. +To use it, you can add the spring-cloud-stream-schema-server artifact to your project and use the @EnableSchemaRegistryServer annotation, which adds the schema registry server REST controller to your application. +This annotation is intended to be used with Spring Boot web applications, and the listening port of the server is controlled by the server.port property. +The spring.cloud.stream.schema.server.path property can be used to control the root path of the schema server (especially when it is embedded in other applications). +The spring.cloud.stream.schema.server.allowSchemaDeletion boolean property enables the deletion of a schema. By default, this is disabled.

    The schema registry server uses a relational database to store the schemas. +By default, it uses an embedded database. +You can customize the schema storage by using the Spring Boot SQL database and JDBC configuration options.

    The following example shows a Spring Boot application that enables the schema registry:

    @SpringBootApplication
    +@EnableSchemaRegistryServer
    +public class SchemaRegistryServerApplication {
    +    public static void main(String[] args) {
    +        SpringApplication.run(SchemaRegistryServerApplication.class, args);
    +    }
    +}

    33.5.1 Schema Registry Server API

    The Schema Registry Server API consists of the following operations:

    Registering a New Schema

    To register a new schema, send a POST request to the / endpoint.

    The / accepts a JSON payload with the following fields:

    • subject: The schema subject
    • format: The schema format
    • definition: The schema definition

    Its response is a schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Retrieving an Existing Schema by Subject, Format, and Version

    To retrieve an existing schema by subject, format, and version, send GET request to the /{subject}/{format}/{version} endpoint.

    Its response is a schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Retrieving an Existing Schema by Subject and Format

    To retrieve an existing schema by subject and format, send a GET request to the /subject/format endpoint.

    Its response is a list of schemas with each schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Retrieving an Existing Schema by ID

    To retrieve a schema by its ID, send a GET request to the /schemas/{id} endpoint.

    Its response is a schema object in JSON, with the following fields:

    • id: The schema ID
    • subject: The schema subject
    • format: The schema format
    • version: The schema version
    • definition: The schema definition

    Deleting a Schema by Subject, Format, and Version

    To delete a schema identified by its subject, format, and version, send a DELETE request to the /{subject}/{format}/{version} endpoint.

    Deleting a Schema by ID

    To delete a schema by its ID, send a DELETE request to the /schemas/{id} endpoint.

    Deleting a Schema by Subject

    DELETE /{subject}

    Delete existing schemas by their subject.

    [Note]Note

    This note applies to users of Spring Cloud Stream 1.1.0.RELEASE only. +Spring Cloud Stream 1.1.0.RELEASE used the table name, schema, for storing Schema objects. Schema is a keyword in a number of database implementations. +To avoid any conflicts in the future, starting with 1.1.1.RELEASE, we have opted for the name SCHEMA_REPOSITORY for the storage table. +Any Spring Cloud Stream 1.1.0.RELEASE users who upgrade should migrate their existing schemas to the new table before upgrading.

    33.5.2 Using Confluent’s Schema Registry

    The default configuration creates a DefaultSchemaRegistryClient bean. +If you want to use the Confluent schema registry, you need to create a bean of type ConfluentSchemaRegistryClient, which supersedes the one configured by default by the framework. The following example shows how to create such a bean:

    @Bean
    +public SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.stream.schemaRegistryClient.endpoint}") String endpoint){
    +  ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient();
    +  client.setEndpoint(endpoint);
    +  return client;
    +}
    [Note]Note

    The ConfluentSchemaRegistryClient is tested against Confluent platform version 4.0.0.

    33.6 Schema Registration and Resolution

    To better understand how Spring Cloud Stream registers and resolves new schemas and its use of Avro schema comparison features, we provide two separate subsections:

    33.6.1 Schema Registration Process (Serialization)

    The first part of the registration process is extracting a schema from the payload that is being sent over a channel. +Avro types such as SpecificRecord or GenericRecord already contain a schema, which can be retrieved immediately from the instance. +In the case of POJOs, a schema is inferred if the spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled property is set to true (the default).

    Figure 33.1. Schema Writer Resolution Process

    schema resolution

    Ones a schema is obtained, the converter loads its metadata (version) from the remote server. +First, it queries a local cache. If no result is found, it submits the data to the server, which replies with versioning information. +The converter always caches the results to avoid the overhead of querying the Schema Server for every new message that needs to be serialized.

    Figure 33.2. Schema Registration Process

    registration

    With the schema version information, the converter sets the contentType header of the message to carry the version information — for example: application/vnd.user.v1+avro.

    33.6.2 Schema Resolution Process (Deserialization)

    When reading messages that contain version information (that is, a contentType header with a scheme like the one described under Section 33.6.1, “Schema Registration Process (Serialization)”), the converter queries the Schema server to fetch the writer schema of the message. +Once it has found the correct schema of the incoming message, it retrieves the reader schema and, by using Avro’s schema resolution support, reads it into the reader definition (setting defaults and any missing properties).

    Figure 33.3. Schema Reading Resolution Process

    schema reading

    [Note]Note

    You should understand the difference between a writer schema (the application that wrote the message) and a reader schema (the receiving application). +We suggest taking a moment to read the Avro terminology and understand the process. +Spring Cloud Stream always fetches the writer schema to determine how to read a message. +If you want to get Avro’s schema evolution support working, you need to make sure that a readerSchema was properly set for your application.

    34. Inter-Application Communication

    Spring Cloud Stream enables communication between applications. Inter-application communication is a complex issue spanning several concerns, as described in the following topics:

    34.1 Connecting Multiple Application Instances

    While Spring Cloud Stream makes it easy for individual Spring Boot applications to connect to messaging systems, the typical scenario for Spring Cloud Stream is the creation of multi-application pipelines, where microservice applications send data to each other. +You can achieve this scenario by correlating the input and output destinations of adjacent applications.

    Suppose a design calls for the Time Source application to send data to the Log Sink application. You could use a common destination named ticktock for bindings within both applications.

    Time Source (that has the channel name output) would set the following property:

    spring.cloud.stream.bindings.output.destination=ticktock

    Log Sink (that has the channel name input) would set the following property:

    spring.cloud.stream.bindings.input.destination=ticktock

    34.2 Instance Index and Instance Count

    When scaling up Spring Cloud Stream applications, each instance can receive information about how many other instances of the same application exist and what its own instance index is. +Spring Cloud Stream does this through the spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex properties. +For example, if there are three instances of a HDFS sink application, all three instances have spring.cloud.stream.instanceCount set to 3, and the individual applications have spring.cloud.stream.instanceIndex set to 0, 1, and 2, respectively.

    When Spring Cloud Stream applications are deployed through Spring Cloud Data Flow, these properties are configured automatically; when Spring Cloud Stream applications are launched independently, these properties must be set correctly. +By default, spring.cloud.stream.instanceCount is 1, and spring.cloud.stream.instanceIndex is 0.

    In a scaled-up scenario, correct configuration of these two properties is important for addressing partitioning behavior (see below) in general, and the two properties are always required by certain binders (for example, the Kafka binder) in order to ensure that data are split correctly across multiple consumer instances.

    34.3 Partitioning

    Partitioning in Spring Cloud Stream consists of two tasks:

    34.3.1 Configuring Output Bindings for Partitioning

    You can configure an output binding to send partitioned data by setting one and only one of its partitionKeyExpression or partitionKeyExtractorName properties, as well as its partitionCount property.

    For example, the following is a valid and typical configuration:

    spring.cloud.stream.bindings.output.producer.partitionKeyExpression=payload.id
    +spring.cloud.stream.bindings.output.producer.partitionCount=5

    Based on that example configuration, data is sent to the target partition by using the following logic.

    A partition key’s value is calculated for each message sent to a partitioned output channel based on the partitionKeyExpression. +The partitionKeyExpression is a SpEL expression that is evaluated against the outbound message for extracting the partitioning key.

    If a SpEL expression is not sufficient for your needs, you can instead calculate the partition key value by providing an implementation of org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy and configuring it as a bean (by using the @Bean annotation). +If you have more then one bean of type org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy available in the Application Context, you can further filter it by specifying its name with the partitionKeyExtractorName property, as shown in the following example:

    --spring.cloud.stream.bindings.output.producer.partitionKeyExtractorName=customPartitionKeyExtractor
    +--spring.cloud.stream.bindings.output.producer.partitionCount=5
    +. . .
    +@Bean
    +public CustomPartitionKeyExtractorClass customPartitionKeyExtractor() {
    +    return new CustomPartitionKeyExtractorClass();
    +}
    [Note]Note

    In previous versions of Spring Cloud Stream, you could specify the implementation of org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy by setting the spring.cloud.stream.bindings.output.producer.partitionKeyExtractorClass property. +Since version 2.0, this property is deprecated, and support for it will be removed in a future version.

    Once the message key is calculated, the partition selection process determines the target partition as a value between 0 and partitionCount - 1. +The default calculation, applicable in most scenarios, is based on the following formula: key.hashCode() % partitionCount. +This can be customized on the binding, either by setting a SpEL expression to be evaluated against the 'key' (through the partitionSelectorExpression property) or by configuring an implementation of org.springframework.cloud.stream.binder.PartitionSelectorStrategy as a bean (by using the @Bean annotation). +Similar to the PartitionKeyExtractorStrategy, you can further filter it by using the spring.cloud.stream.bindings.output.producer.partitionSelectorName property when more than one bean of this type is available in the Application Context, as shown in the following example:

    --spring.cloud.stream.bindings.output.producer.partitionSelectorName=customPartitionSelector
    +. . .
    +@Bean
    +public CustomPartitionSelectorClass customPartitionSelector() {
    +    return new CustomPartitionSelectorClass();
    +}
    [Note]Note

    In previous versions of Spring Cloud Stream you could specify the implementation of org.springframework.cloud.stream.binder.PartitionSelectorStrategy by setting the spring.cloud.stream.bindings.output.producer.partitionSelectorClass property. +Since version 2.0, this property is deprecated and support for it will be removed in a future version.

    34.3.2 Configuring Input Bindings for Partitioning

    An input binding (with the channel name input) is configured to receive partitioned data by setting its partitioned property, as well as the instanceIndex and instanceCount properties on the application itself, as shown in the following example:

    spring.cloud.stream.bindings.input.consumer.partitioned=true
    +spring.cloud.stream.instanceIndex=3
    +spring.cloud.stream.instanceCount=5

    The instanceCount value represents the total number of application instances between which the data should be partitioned. +The instanceIndex must be a unique value across the multiple instances, with a value between 0 and instanceCount - 1. +The instance index helps each application instance to identify the unique partition(s) from which it receives data. +It is required by binders using technology that does not support partitioning natively. +For example, with RabbitMQ, there is a queue for each partition, with the queue name containing the instance index. +With Kafka, if autoRebalanceEnabled is true (default), Kafka takes care of distributing partitions across instances, and these properties are not required. +If autoRebalanceEnabled is set to false, the instanceCount and instanceIndex are used by the binder to determine which partition(s) the instance subscribes to (you must have at least as many partitions as there are instances). +The binder allocates the partitions instead of Kafka. +This might be useful if you want messages for a particular partition to always go to the same instance. +When a binder configuration requires them, it is important to set both values correctly in order to ensure that all of the data is consumed and that the application instances receive mutually exclusive datasets.

    While a scenario in which using multiple instances for partitioned data processing may be complex to set up in a standalone case, Spring Cloud Dataflow can simplify the process significantly by populating both the input and output values correctly and by letting you rely on the runtime infrastructure to provide information about the instance index and instance count.

    35. Testing

    Spring Cloud Stream provides support for testing your microservice applications without connecting to a messaging system. +You can do that by using the TestSupportBinder provided by the spring-cloud-stream-test-support library, which can be added as a test dependency to the application, as shown in the following example:

       <dependency>
    +       <groupId>org.springframework.cloud</groupId>
    +       <artifactId>spring-cloud-stream-test-support</artifactId>
    +       <scope>test</scope>
    +   </dependency>
    [Note]Note

    The TestSupportBinder uses the Spring Boot autoconfiguration mechanism to supersede the other binders found on the classpath. +Therefore, when adding a binder as a dependency, you must make sure that the test scope is being used.

    The TestSupportBinder lets you interact with the bound channels and inspect any messages sent and received by the application.

    For outbound message channels, the TestSupportBinder registers a single subscriber and retains the messages emitted by the application in a MessageCollector. +They can be retrieved during tests and have assertions made against them.

    You can also send messages to inbound message channels so that the consumer application can consume the messages. +The following example shows how to test both input and output channels on a processor:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
    +public class ExampleTest {
    +
    +  @Autowired
    +  private Processor processor;
    +
    +  @Autowired
    +  private MessageCollector messageCollector;
    +
    +  @Test
    +  @SuppressWarnings("unchecked")
    +  public void testWiring() {
    +    Message<String> message = new GenericMessage<>("hello");
    +    processor.input().send(message);
    +    Message<String> received = (Message<String>) messageCollector.forChannel(processor.output()).poll();
    +    assertThat(received.getPayload(), equalTo("hello world"));
    +  }
    +
    +
    +  @SpringBootApplication
    +  @EnableBinding(Processor.class)
    +  public static class MyProcessor {
    +
    +    @Autowired
    +    private Processor channels;
    +
    +    @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
    +    public String transform(String in) {
    +      return in + " world";
    +    }
    +  }
    +}

    In the preceding example, we create an application that has an input channel and an output channel, both bound through the Processor interface. +The bound interface is injected into the test so that we can have access to both channels. +We send a message on the input channel, and we use the MessageCollector provided by Spring Cloud Stream’s test support to capture that the message has been sent to the output channel as a result. +Once we have received the message, we can validate that the component functions correctly.

    35.1 Disabling the Test Binder Autoconfiguration

    The intent behind the test binder superseding all the other binders on the classpath is to make it easy to test your applications without making changes to your production dependencies. +In some cases (for example, integration tests) it is useful to use the actual production binders instead, and that requires disabling the test binder autoconfiguration. +To do so, you can exclude the org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration class by using one of the Spring Boot autoconfiguration exclusion mechanisms, as shown in the following example:

        @SpringBootApplication(exclude = TestSupportBinderAutoConfiguration.class)
    +    @EnableBinding(Processor.class)
    +    public static class MyProcessor {
    +
    +        @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT)
    +        public String transform(String in) {
    +            return in + " world";
    +        }
    +    }

    When autoconfiguration is disabled, the test binder is available on the classpath, and its defaultCandidate property is set to false so that it does not interfere with the regular user configuration. It can be referenced under the name, test, as shown in the following example:

    spring.cloud.stream.defaultBinder=test

    36. Health Indicator

    Spring Cloud Stream provides a health indicator for binders. +It is registered under the name binders and can be enabled or disabled by setting the management.health.binders.enabled property.

    By default management.health.binders.enabled is set to false. +Setting management.health.binders.enabled to true enables the health indicator, allowing you to access the /health endpoint to retrieve the binder health indicators.

    Health indicators are binder-specific and certain binder implementations may not necessarily provide a health indicator.

    37. Metrics Emitter

    Spring Boot Actuator provides dependency management and auto-configuration for Micrometer, an application metrics +facade that supports numerous monitoring systems.

    Spring Cloud Stream provides support for emitting any available micrometer-based metrics to a binding destination, allowing for periodic +collection of metric data from stream applications without relying on polling individual endpoints.

    Metrics Emitter is activated by defining the spring.cloud.stream.bindings.applicationMetrics.destination property, +which specifies the name of the binding destination used by the current binder to publish metric messages.

    For example:

    spring.cloud.stream.bindings.applicationMetrics.destination=myMetricDestination

    The preceding example instructs the binder to bind to myMetricDestination (that is, Rabbit exchange, Kafka topic, and others).

    The following properties can be used for customizing the emission of metrics:

    spring.cloud.stream.metrics.key

    The name of the metric being emitted. Should be a unique value per application.

    Default: ${spring.application.name:${vcap.application.name:${spring.config.name:application}}}

    spring.cloud.stream.metrics.properties

    Allows white listing application properties that are added to the metrics payload

    Default: null.

    spring.cloud.stream.metrics.meter-filter

    Pattern to control the 'meters' one wants to capture. +For example, specifying spring.integration.* captures metric information for meters whose name starts with spring.integration.

    Default: all 'meters' are captured.

    spring.cloud.stream.metrics.schedule-interval

    Interval to control the rate of publishing metric data.

    Default: 1 min

    Consider the following:

    java -jar time-source.jar \
    +    --spring.cloud.stream.bindings.applicationMetrics.destination=someMetrics \
    +    --spring.cloud.stream.metrics.properties=spring.application** \
    +    --spring.cloud.stream.metrics.meter-filter=spring.integration.*

    The following example shows the payload of the data published to the binding destination as a result of the preceding command:

    {
    +	"name": "application",
    +	"createdTime": "2018-03-23T14:48:12.700Z",
    +	"properties": {
    +	},
    +	"metrics": [
    +		{
    +			"id": {
    +				"name": "spring.integration.send",
    +				"tags": [
    +					{
    +						"key": "exception",
    +						"value": "none"
    +					},
    +					{
    +						"key": "name",
    +						"value": "input"
    +					},
    +					{
    +						"key": "result",
    +						"value": "success"
    +					},
    +					{
    +						"key": "type",
    +						"value": "channel"
    +					}
    +				],
    +				"type": "TIMER",
    +				"description": "Send processing time",
    +				"baseUnit": "milliseconds"
    +			},
    +			"timestamp": "2018-03-23T14:48:12.697Z",
    +			"sum": 130.340546,
    +			"count": 6,
    +			"mean": 21.72342433333333,
    +			"upper": 116.176299,
    +			"total": 130.340546
    +		}
    +	]
    +}
    [Note]Note

    Given that the format of the Metric message has slightly changed after migrating to Micrometer, the published message will also have +a STREAM_CLOUD_STREAM_VERSION header set to 2.x to help distinguish between Metric messages from the older versions of the Spring Cloud Stream.

    38. Samples

    For Spring Cloud Stream samples, see the spring-cloud-stream-samples repository on GitHub.

    38.1 Deploying Stream Applications on CloudFoundry

    On CloudFoundry, services are usually exposed through a special environment variable called VCAP_SERVICES.

    When configuring your binder connections, you can use the values from an environment variable as explained on the dataflow Cloud Foundry Server docs.

    Part VI. Binder Implementations

    39. Apache Kafka Binder

    39.1 Usage

    To use Apache Kafka binder, you need to add spring-cloud-stream-binder-kafka as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-kafka</artifactId>
    +</dependency>

    Alternatively, you can also use the Spring Cloud Stream Kafka Starter, as shown inn the following example for Maven:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-starter-stream-kafka</artifactId>
    +</dependency>

    39.2 Apache Kafka Binder Overview

    The following image shows a simplified diagram of how the Apache Kafka binder operates:

    Figure 39.1. Kafka Binder

    kafka binder

    The Apache Kafka Binder implementation maps each destination to an Apache Kafka topic. +The consumer group maps directly to the same Apache Kafka concept. +Partitioning also maps directly to Apache Kafka partitions as well.

    The binder currently uses the Apache Kafka kafka-clients 1.0.0 jar and is designed to be used with a broker of at least that version. +This client can communicate with older brokers (see the Kafka documentation), but certain features may not be available. +For example, with versions earlier than 0.11.x.x, native headers are not supported. +Also, 0.11.x.x does not support the autoAddPartitions property.

    39.3 Configuration Options

    This section contains the configuration options used by the Apache Kafka binder.

    For common configuration options and properties pertaining to binder, see the core documentation.

    39.3.1 Kafka Binder Properties

    spring.cloud.stream.kafka.binder.brokers

    A list of brokers to which the Kafka binder connects.

    Default: localhost.

    spring.cloud.stream.kafka.binder.defaultBrokerPort

    brokers allows hosts specified with or without port information (for example, host1,host2:port2). +This sets the default port when no port is configured in the broker list.

    Default: 9092.

    spring.cloud.stream.kafka.binder.configuration

    Key/Value map of client properties (both producers and consumer) passed to all clients created by the binder. +Due to the fact that these properties are used by both producers and consumers, usage should be restricted to common properties — for example, security settings. +Properties here supersede any properties set in boot.

    Default: Empty map.

    spring.cloud.stream.kafka.binder.consumerProperties

    Key/Value map of arbitrary Kafka client consumer properties. +Properties here supersede any properties set in boot and in the configuration property above.

    Default: Empty map.

    spring.cloud.stream.kafka.binder.headers

    The list of custom headers that are transported by the binder. +Only required when communicating with older applications (⇐ 1.3.x) with a kafka-clients version < 0.11.0.0. Newer versions support headers natively.

    Default: empty.

    spring.cloud.stream.kafka.binder.healthTimeout

    The time to wait to get partition information, in seconds. +Health reports as down if this timer expires.

    Default: 10.

    spring.cloud.stream.kafka.binder.requiredAcks

    The number of required acks on the broker. +See the Kafka documentation for the producer acks property.

    Default: 1.

    spring.cloud.stream.kafka.binder.minPartitionCount

    Effective only if autoCreateTopics or autoAddPartitions is set. +The global minimum number of partitions that the binder configures on topics on which it produces or consumes data. +It can be superseded by the partitionCount setting of the producer or by the value of instanceCount * concurrency settings of the producer (if either is larger).

    Default: 1.

    spring.cloud.stream.kafka.binder.producerProperties

    Key/Value map of arbitrary Kafka client producer properties. +Properties here supersede any properties set in boot and in the configuration property above.

    Default: Empty map.

    spring.cloud.stream.kafka.binder.replicationFactor

    The replication factor of auto-created topics if autoCreateTopics is active. +Can be overridden on each binding.

    Default: 1.

    spring.cloud.stream.kafka.binder.autoCreateTopics

    If set to true, the binder creates new topics automatically. +If set to false, the binder relies on the topics being already configured. +In the latter case, if the topics do not exist, the binder fails to start.

    [Note]Note

    This setting is independent of the auto.topic.create.enable setting of the broker and does not influence it. +If the server is set to auto-create topics, they may be created as part of the metadata retrieval request, with default broker settings.

    Default: true.

    spring.cloud.stream.kafka.binder.autoAddPartitions

    If set to true, the binder creates new partitions if required. +If set to false, the binder relies on the partition size of the topic being already configured. +If the partition count of the target topic is smaller than the expected value, the binder fails to start.

    Default: false.

    spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix

    Enables transactions in the binder. See transaction.id in the Kafka documentation and Transactions in the spring-kafka documentation. +When transactions are enabled, individual producer properties are ignored and all producers use the spring.cloud.stream.kafka.binder.transaction.producer.* properties.

    Default null (no transactions)

    spring.cloud.stream.kafka.binder.transaction.producer.*

    Global producer properties for producers in a transactional binder. +See spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix and Section 39.3.3, “Kafka Producer Properties” and the general producer properties supported by all binders.

    Default: See individual producer properties.

    spring.cloud.stream.kafka.binder.headerMapperBeanName

    The bean name of a KafkaHeaderMapper used for mapping spring-messaging headers to and from Kafka headers. +Use this, for example, if you wish to customize the trusted packages in a DefaultKafkaHeaderMapper that uses JSON deserialization for the headers.

    Default: none.

    39.3.2 Kafka Consumer Properties

    The following properties are available for Kafka consumers only and +must be prefixed with spring.cloud.stream.kafka.bindings.<channelName>.consumer..

    admin.configuration

    A Map of Kafka topic properties used when provisioning topics — for example, spring.cloud.stream.kafka.bindings.input.consumer.admin.configuration.message.format.version=0.9.0.0

    Default: none.

    admin.replicas-assignment

    A Map<Integer, List<Integer>> of replica assignments, with the key being the partition and the value being the assignments. +Used when provisioning new topics. +See the NewTopic Javadocs in the kafka-clients jar.

    Default: none.

    admin.replication-factor

    The replication factor to use when provisioning topics. Overrides the binder-wide setting. +Ignored if replicas-assignments is present.

    Default: none (the binder-wide default of 1 is used).

    autoRebalanceEnabled

    When true, topic partitions is automatically rebalanced between the members of a consumer group. +When false, each consumer is assigned a fixed set of partitions based on spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex. +This requires both the spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex properties to be set appropriately on each launched instance. +The value of the spring.cloud.stream.instanceCount property must typically be greater than 1 in this case.

    Default: true.

    ackEachRecord

    When autoCommitOffset is true, this setting dictates whether to commit the offset after each record is processed. +By default, offsets are committed after all records in the batch of records returned by consumer.poll() have been processed. +The number of records returned by a poll can be controlled with the max.poll.records Kafka property, which is set through the consumer configuration property. +Setting this to true may cause a degradation in performance, but doing so reduces the likelihood of redelivered records when a failure occurs. +Also, see the binder requiredAcks property, which also affects the performance of committing offsets.

    Default: false.

    autoCommitOffset

    Whether to autocommit offsets when a message has been processed. +If set to false, a header with the key kafka_acknowledgment of the type org.springframework.kafka.support.Acknowledgment header is present in the inbound message. +Applications may use this header for acknowledging messages. +See the examples section for details. +When this property is set to false, Kafka binder sets the ack mode to org.springframework.kafka.listener.AbstractMessageListenerContainer.AckMode.MANUAL and the application is responsible for acknowledging records. +Also see ackEachRecord.

    Default: true.

    autoCommitOnError

    Effective only if autoCommitOffset is set to true. +If set to false, it suppresses auto-commits for messages that result in errors and commits only for successful messages. It allows a stream to automatically replay from the last successfully processed message, in case of persistent failures. +If set to true, it always auto-commits (if auto-commit is enabled). +If not set (the default), it effectively has the same value as enableDlq, auto-committing erroneous messages if they are sent to a DLQ and not committing them otherwise.

    Default: not set.

    resetOffsets

    Whether to reset offsets on the consumer to the value provided by startOffset.

    Default: false.

    startOffset

    The starting offset for new groups. +Allowed values: earliest and latest. +If the consumer group is set explicitly for the consumer 'binding' (through spring.cloud.stream.bindings.<channelName>.group), 'startOffset' is set to earliest. Otherwise, it is set to latest for the anonymous consumer group. +Also see resetOffsets (earlier in this list).

    Default: null (equivalent to earliest).

    enableDlq

    When set to true, it enables DLQ behavior for the consumer. +By default, messages that result in errors are forwarded to a topic named error.<destination>.<group>. +The DLQ topic name can be configurable by setting the dlqName property. +This provides an alternative option to the more common Kafka replay scenario for the case when the number of errors is relatively small and replaying the entire original topic may be too cumbersome. +See Section 39.6, “Dead-Letter Topic Processing” processing for more information. +Starting with version 2.0, messages sent to the DLQ topic are enhanced with the following headers: x-original-topic, x-exception-message, and x-exception-stacktrace as byte[]. +Not allowed when destinationIsPattern is true.

    Default: false.

    configuration

    Map with a key/value pair containing generic Kafka consumer properties.

    Default: Empty map.

    dlqName

    The name of the DLQ topic to receive the error messages.

    Default: null (If not specified, messages that result in errors are forwarded to a topic named error.<destination>.<group>).

    dlqProducerProperties

    Using this, DLQ-specific producer properties can be set. +All the properties available through kafka producer properties can be set through this property.

    Default: Default Kafka producer properties.

    standardHeaders

    Indicates which standard headers are populated by the inbound channel adapter. +Allowed values: none, id, timestamp, or both. +Useful if using native deserialization and the first component to receive a message needs an id (such as an aggregator that is configured to use a JDBC message store).

    Default: none

    converterBeanName

    The name of a bean that implements RecordMessageConverter. Used in the inbound channel adapter to replace the default MessagingMessageConverter.

    Default: null

    idleEventInterval

    The interval, in milliseconds, between events indicating that no messages have recently been received. +Use an ApplicationListener<ListenerContainerIdleEvent> to receive these events. +See the section called “Example: Pausing and Resuming the Consumer” for a usage example.

    Default: 30000

    destinationIsPattern

    When true, the destination is treated as a regular expression Pattern used to match topic names by the broker. +When true, topics are not provisioned, and enableDlq is not allowed, because the binder does not know the topic names during the provisioning phase. +Note, the time taken to detect new topics that match the pattern is controlled by the consumer property metadata.max.age.ms, which (at the time of writing) defaults to 300,000ms (5 minutes). +This can be configured using the configuration property above.

    Default: false

    39.3.3 Kafka Producer Properties

    The following properties are available for Kafka producers only and +must be prefixed with spring.cloud.stream.kafka.bindings.<channelName>.producer..

    admin.configuration

    A Map of Kafka topic properties used when provisioning new topics — for example, spring.cloud.stream.kafka.bindings.input.consumer.admin.configuration.message.format.version=0.9.0.0

    Default: none.

    admin.replicas-assignment

    A Map<Integer, List<Integer>> of replica assignments, with the key being the partition and the value being the assignments. +Used when provisioning new topics. +See NewTopic javadocs in the kafka-clients jar.

    Default: none.

    admin.replication-factor

    The replication factor to use when provisioning new topics. Overrides the binder-wide setting. +Ignored if replicas-assignments is present.

    Default: none (the binder-wide default of 1 is used).

    bufferSize

    Upper limit, in bytes, of how much data the Kafka producer attempts to batch before sending.

    Default: 16384.

    sync

    Whether the producer is synchronous.

    Default: false.

    batchTimeout

    How long the producer waits to allow more messages to accumulate in the same batch before sending the messages. +(Normally, the producer does not wait at all and simply sends all the messages that accumulated while the previous send was in progress.) A non-zero value may increase throughput at the expense of latency.

    Default: 0.

    messageKeyExpression

    A SpEL expression evaluated against the outgoing message used to populate the key of the produced Kafka message — for example, headers['myKey']. +The payload cannot be used because, by the time this expression is evaluated, the payload is already in the form of a byte[].

    Default: none.

    headerPatterns

    A comma-delimited list of simple patterns to match Spring messaging headers to be mapped to the Kafka Headers in the ProducerRecord. +Patterns can begin or end with the wildcard character (asterisk). +Patterns can be negated by prefixing with !. +Matching stops after the first match (positive or negative). +For example !ask,as* will pass ash but not ask. +id and timestamp are never mapped.

    Default: * (all headers - except the id and timestamp)

    configuration

    Map with a key/value pair containing generic Kafka producer properties.

    Default: Empty map.

    [Note]Note

    The Kafka binder uses the partitionCount setting of the producer as a hint to create a topic with the given partition count (in conjunction with the minPartitionCount, the maximum of the two being the value being used). +Exercise caution when configuring both minPartitionCount for a binder and partitionCount for an application, as the larger value is used. +If a topic already exists with a smaller partition count and autoAddPartitions is disabled (the default), the binder fails to start. +If a topic already exists with a smaller partition count and autoAddPartitions is enabled, new partitions are added. +If a topic already exists with a larger number of partitions than the maximum of (minPartitionCount or partitionCount), the existing partition count is used.

    39.3.4 Usage examples

    In this section, we show the use of the preceding properties for specific scenarios.

    Example: Setting autoCommitOffset to false and Relying on Manual Acking

    This example illustrates how one may manually acknowledge offsets in a consumer application.

    This example requires that spring.cloud.stream.kafka.bindings.input.consumer.autoCommitOffset be set to false. +Use the corresponding input channel name for your example.

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class ManuallyAcknowdledgingConsumer {
    +
    + public static void main(String[] args) {
    +     SpringApplication.run(ManuallyAcknowdledgingConsumer.class, args);
    + }
    +
    + @StreamListener(Sink.INPUT)
    + public void process(Message<?> message) {
    +     Acknowledgment acknowledgment = message.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class);
    +     if (acknowledgment != null) {
    +         System.out.println("Acknowledgment provided");
    +         acknowledgment.acknowledge();
    +     }
    + }
    +}

    Example: Security Configuration

    Apache Kafka 0.9 supports secure connections between client and brokers. +To take advantage of this feature, follow the guidelines in the Apache Kafka Documentation as well as the Kafka 0.9 security guidelines from the Confluent documentation. +Use the spring.cloud.stream.kafka.binder.configuration option to set security properties for all clients created by the binder.

    For example, to set security.protocol to SASL_SSL, set the following property:

    spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_SSL

    All the other security properties can be set in a similar manner.

    When using Kerberos, follow the instructions in the reference documentation for creating and referencing the JAAS configuration.

    Spring Cloud Stream supports passing JAAS configuration information to the application by using a JAAS configuration file and using Spring Boot properties.

    Using JAAS Configuration Files

    The JAAS and (optionally) krb5 file locations can be set for Spring Cloud Stream applications by using system properties. +The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using a JAAS configuration file:

     java -Djava.security.auth.login.config=/path.to/kafka_client_jaas.conf -jar log.jar \
    +   --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \
    +   --spring.cloud.stream.bindings.input.destination=stream.ticktock \
    +   --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT
    Using Spring Boot Properties

    As an alternative to having a JAAS configuration file, Spring Cloud Stream provides a mechanism for setting up the JAAS configuration for Spring Cloud Stream applications by using Spring Boot properties.

    The following properties can be used to configure the login context of the Kafka client:

    spring.cloud.stream.kafka.binder.jaas.loginModule

    The login module name. Not necessary to be set in normal cases.

    Default: com.sun.security.auth.module.Krb5LoginModule.

    spring.cloud.stream.kafka.binder.jaas.controlFlag

    The control flag of the login module.

    Default: required.

    spring.cloud.stream.kafka.binder.jaas.options

    Map with a key/value pair containing the login module options.

    Default: Empty map.

    The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using Spring Boot configuration properties:

     java --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \
    +   --spring.cloud.stream.bindings.input.destination=stream.ticktock \
    +   --spring.cloud.stream.kafka.binder.autoCreateTopics=false \
    +   --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT \
    +   --spring.cloud.stream.kafka.binder.jaas.options.useKeyTab=true \
    +   --spring.cloud.stream.kafka.binder.jaas.options.storeKey=true \
    +   --spring.cloud.stream.kafka.binder.jaas.options.keyTab=/etc/security/keytabs/kafka_client.keytab \
    +   --spring.cloud.stream.kafka.binder.jaas.options.principal=kafka-client-1@EXAMPLE.COM

    The preceding example represents the equivalent of the following JAAS file:

    KafkaClient {
    +    com.sun.security.auth.module.Krb5LoginModule required
    +    useKeyTab=true
    +    storeKey=true
    +    keyTab="/etc/security/keytabs/kafka_client.keytab"
    +    principal="kafka-client-1@EXAMPLE.COM";
    +};

    If the topics required already exist on the broker or will be created by an administrator, autocreation can be turned off and only client JAAS properties need to be sent.

    [Note]Note

    Do not mix JAAS configuration files and Spring Boot properties in the same application. +If the -Djava.security.auth.login.config system property is already present, Spring Cloud Stream ignores the Spring Boot properties.

    [Note]Note

    Be careful when using the autoCreateTopics and autoAddPartitions with Kerberos. +Usually, applications may use principals that do not have administrative rights in Kafka and Zookeeper. +Consequently, relying on Spring Cloud Stream to create/modify topics may fail. +In secure environments, we strongly recommend creating topics and managing ACLs administratively by using Kafka tooling.

    Example: Pausing and Resuming the Consumer

    If you wish to suspend consumption but not cause a partition rebalance, you can pause and resume the consumer. +This is facilitated by adding the Consumer as a parameter to your @StreamListener. +To resume, you need an ApplicationListener for ListenerContainerIdleEvent instances. +The frequency at which events are published is controlled by the idleEventInterval property. +Since the consumer is not thread-safe, you must call these methods on the calling thread.

    The following simple application shows how to pause and resume:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class Application {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(Application.class, args);
    +	}
    +
    +	@StreamListener(Sink.INPUT)
    +	public void in(String in, @Header(KafkaHeaders.CONSUMER) Consumer<?, ?> consumer) {
    +		System.out.println(in);
    +		consumer.pause(Collections.singleton(new TopicPartition("myTopic", 0)));
    +	}
    +
    +	@Bean
    +	public ApplicationListener<ListenerContainerIdleEvent> idleListener() {
    +		return event -> {
    +			System.out.println(event);
    +			if (event.getConsumer().paused().size() > 0) {
    +				event.getConsumer().resume(event.getConsumer().paused());
    +			}
    +		};
    +	}
    +
    +}

    39.4 Error Channels

    Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See Section 29.4, “Error Handling” for more information.

    The payload of the ErrorMessage for a send failure is a KafkaSendFailureException with properties:

    • failedMessage: The Spring Messaging Message<?> that failed to be sent.
    • record: The raw ProducerRecord that was created from the failedMessage

    There is no automatic handling of producer exceptions (such as sending to a Dead-Letter queue). +You can consume these exceptions with your own Spring Integration flow.

    39.5 Kafka Metrics

    Kafka binder module exposes the following metrics:

    spring.cloud.stream.binder.kafka.offset: This metric indicates how many messages have not been yet consumed from a given binder’s topic by a given consumer group. +The metrics provided are based on the Mircometer metrics library. The metric contains the consumer group information, topic and the actual lag in committed offset from the latest offset on the topic. +This metric is particularly useful for providing auto-scaling feedback to a PaaS platform.

    39.6 Dead-Letter Topic Processing

    Because you cannot anticipate how users would want to dispose of dead-lettered messages, the framework does not provide any standard mechanism to handle them. +If the reason for the dead-lettering is transient, you may wish to route the messages back to the original topic. +However, if the problem is a permanent issue, that could cause an infinite loop. +The sample Spring Boot application within this topic is an example of how to route those messages back to the original topic, but it moves them to a parking lot topic after three attempts. +The application is another spring-cloud-stream application that reads from the dead-letter topic. +It terminates when no messages are received for 5 seconds.

    The examples assume the original destination is so8400out and the consumer group is so8400.

    There are a couple of strategies to consider:

    • Consider running the rerouting only when the main application is not running. +Otherwise, the retries for transient errors are used up very quickly.
    • Alternatively, use a two-stage approach: Use this application to route to a third topic and another to route from there back to the main topic.

    The following code listings show the sample application:

    application.properties.  +

    spring.cloud.stream.bindings.input.group=so8400replay
    +spring.cloud.stream.bindings.input.destination=error.so8400out.so8400
    +
    +spring.cloud.stream.bindings.output.destination=so8400out
    +spring.cloud.stream.bindings.output.producer.partitioned=true
    +
    +spring.cloud.stream.bindings.parkingLot.destination=so8400in.parkingLot
    +spring.cloud.stream.bindings.parkingLot.producer.partitioned=true
    +
    +spring.cloud.stream.kafka.binder.configuration.auto.offset.reset=earliest
    +
    +spring.cloud.stream.kafka.binder.headers=x-retries

    +

    Application.  +

    @SpringBootApplication
    +@EnableBinding(TwoOutputProcessor.class)
    +public class ReRouteDlqKApplication implements CommandLineRunner {
    +
    +    private static final String X_RETRIES_HEADER = "x-retries";
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(ReRouteDlqKApplication.class, args).close();
    +    }
    +
    +    private final AtomicInteger processed = new AtomicInteger();
    +
    +    @Autowired
    +    private MessageChannel parkingLot;
    +
    +    @StreamListener(Processor.INPUT)
    +    @SendTo(Processor.OUTPUT)
    +    public Message<?> reRoute(Message<?> failed) {
    +        processed.incrementAndGet();
    +        Integer retries = failed.getHeaders().get(X_RETRIES_HEADER, Integer.class);
    +        if (retries == null) {
    +            System.out.println("First retry for " + failed);
    +            return MessageBuilder.fromMessage(failed)
    +                    .setHeader(X_RETRIES_HEADER, new Integer(1))
    +                    .setHeader(BinderHeaders.PARTITION_OVERRIDE,
    +                            failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
    +                    .build();
    +        }
    +        else if (retries.intValue() < 3) {
    +            System.out.println("Another retry for " + failed);
    +            return MessageBuilder.fromMessage(failed)
    +                    .setHeader(X_RETRIES_HEADER, new Integer(retries.intValue() + 1))
    +                    .setHeader(BinderHeaders.PARTITION_OVERRIDE,
    +                            failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
    +                    .build();
    +        }
    +        else {
    +            System.out.println("Retries exhausted for " + failed);
    +            parkingLot.send(MessageBuilder.fromMessage(failed)
    +                    .setHeader(BinderHeaders.PARTITION_OVERRIDE,
    +                            failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
    +                    .build());
    +        }
    +        return null;
    +    }
    +
    +    @Override
    +    public void run(String... args) throws Exception {
    +        while (true) {
    +            int count = this.processed.get();
    +            Thread.sleep(5000);
    +            if (count == this.processed.get()) {
    +                System.out.println("Idle, terminating");
    +                return;
    +            }
    +        }
    +    }
    +
    +    public interface TwoOutputProcessor extends Processor {
    +
    +        @Output("parkingLot")
    +        MessageChannel parkingLot();
    +
    +    }
    +
    +}

    +

    39.7 Partitioning with the Kafka Binder

    Apache Kafka supports topic partitioning natively.

    Sometimes it is advantageous to send data to specific partitions — for example, when you want to strictly order message processing (all messages for a particular customer should go to the same partition).

    The following example shows how to configure the producer and consumer side:

    @SpringBootApplication
    +@EnableBinding(Source.class)
    +public class KafkaPartitionProducerApplication {
    +
    +    private static final Random RANDOM = new Random(System.currentTimeMillis());
    +
    +    private static final String[] data = new String[] {
    +            "foo1", "bar1", "qux1",
    +            "foo2", "bar2", "qux2",
    +            "foo3", "bar3", "qux3",
    +            "foo4", "bar4", "qux4",
    +            };
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(KafkaPartitionProducerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedRate = "5000"))
    +    public Message<?> generate() {
    +        String value = data[RANDOM.nextInt(data.length)];
    +        System.out.println("Sending: " + value);
    +        return MessageBuilder.withPayload(value)
    +                .setHeader("partitionKey", value)
    +                .build();
    +    }
    +
    +}

    application.yml.  +

    spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        output:
    +          destination: partitioned.topic
    +          producer:
    +            partitioned: true
    +            partition-key-expression: headers['partitionKey']
    +            partition-count: 12

    +

    [Important]Important

    The topic must be provisioned to have enough partitions to achieve the desired concurrency for all consumer groups. +The above configuration supports up to 12 consumer instances (6 if their concurrency is 2, 4 if their concurrency is 3, and so on). +It is generally best to over-provision the partitions to allow for future increases in consumers or concurrency.

    [Note]Note

    The preceding configuration uses the default partitioning (key.hashCode() % partitionCount). +This may or may not provide a suitably balanced algorithm, depending on the key values. +You can override this default by using the partitionSelectorExpression or partitionSelectorClass properties.

    Since partitions are natively handled by Kafka, no special configuration is needed on the consumer side. +Kafka allocates partitions across the instances.

    The following Spring Boot application listens to a Kafka stream and prints (to the console) the partition ID to which each message goes:

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class KafkaPartitionConsumerApplication {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(KafkaPartitionConsumerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @StreamListener(Sink.INPUT)
    +    public void listen(@Payload String in, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) {
    +        System.out.println(in + " received from partition " + partition);
    +    }
    +
    +}

    application.yml.  +

    spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        input:
    +          destination: partitioned.topic
    +          group: myGroup

    +

    You can add instances as needed. +Kafka rebalances the partition allocations. +If the instance count (or instance count * concurrency) exceeds the number of partitions, some consumers are idle.

    40. Apache Kafka Streams Binder

    40.1 Usage

    For using the Kafka Streams binder, you just need to add it to your Spring Cloud Stream application, using the following +Maven coordinates:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
    +</dependency>

    40.2 Kafka Streams Binder Overview

    Spring Cloud Stream’s Apache Kafka support also includes a binder implementation designed explicitly for Apache Kafka +Streams binding. With this native integration, a Spring Cloud Stream "processor" application can directly use the +Apache Kafka Streams APIs in the core business logic.

    Kafka Streams binder implementation builds on the foundation provided by the Kafka Streams in Spring Kafka +project.

    Kafka Streams binder provides binding capabilities for the three major types in Kafka Streams - KStream, KTable and GlobalKTable.

    As part of this native integration, the high-level Streams DSL +provided by the Kafka Streams API is available for use in the business logic.

    An early version of the Processor API +support is available as well.

    As noted early-on, Kafka Streams support in Spring Cloud Stream is strictly only available for use in the Processor model. +A model in which the messages read from an inbound topic, business processing can be applied, and the transformed messages +can be written to an outbound topic. It can also be used in Processor applications with a no-outbound destination.

    40.2.1 Streams DSL

    This application consumes data from a Kafka topic (e.g., words), computes word count for each unique word in a 5 seconds +time window, and the computed results are sent to a downstream topic (e.g., counts) for further processing.

    @SpringBootApplication
    +@EnableBinding(KStreamProcessor.class)
    +public class WordCountProcessorApplication {
    +
    +	@StreamListener("input")
    +	@SendTo("output")
    +	public KStream<?, WordCount> process(KStream<?, String> input) {
    +		return input
    +                .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
    +                .groupBy((key, value) -> value)
    +                .windowedBy(TimeWindows.of(5000))
    +                .count(Materialized.as("WordCounts-multi"))
    +                .toStream()
    +                .map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, new Date(key.window().start()), new Date(key.window().end()))));
    +    }
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(WordCountProcessorApplication.class, args);
    +	}

    Once built as a uber-jar (e.g., wordcount-processor.jar), you can run the above example like the following.

    java -jar wordcount-processor.jar  --spring.cloud.stream.bindings.input.destination=words --spring.cloud.stream.bindings.output.destination=counts

    This application will consume messages from the Kafka topic words and the computed results are published to an output +topic counts.

    Spring Cloud Stream will ensure that the messages from both the incoming and outgoing topics are automatically bound as +KStream objects. As a developer, you can exclusively focus on the business aspects of the code, i.e. writing the logic +required in the processor. Setting up the Streams DSL specific configuration required by the Kafka Streams infrastructure +is automatically handled by the framework.

    40.3 Configuration Options

    This section contains the configuration options used by the Kafka Streams binder.

    For common configuration options and properties pertaining to binder, refer to the core documentation.

    40.3.1 Kafka Streams Properties

    The following properties are available at the binder level and must be prefixed with spring.cloud.stream.kafka.streams.binder. +literal.

    configuration
    Map with a key/value pair containing properties pertaining to Apache Kafka Streams API. + This property must be prefixed with spring.cloud.stream.kafka.streams.binder.. +Following are some examples of using this property.
    spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
    +spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
    +spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000

    For more information about all the properties that may go into streams configuration, see StreamsConfig JavaDocs in +Apache Kafka Streams docs.

    brokers

    Broker URL

    Default: localhost

    zkNodes

    Zookeeper URL

    Default: localhost

    serdeError

    Deserialization error handler type. +Possible values are - logAndContinue, logAndFail or sendToDlq

    Default: logAndFail

    applicationId

    Convenient way to set the application.id for the Kafka Streams application globally at the binder level. +If the application contains multiple StreamListener methods, then application.id should be set at the binding level per input binding.

    Default: none

    The following properties are only available for Kafka Streams producers and must be prefixed with spring.cloud.stream.kafka.streams.bindings.<binding name>.producer. literal. +For convenience, if there multiple output bindings and they all require a common value, that can be configured by using the prefix spring.cloud.stream.kafka.streams.default.producer..

    keySerde

    key serde to use

    Default: none.

    valueSerde

    value serde to use

    Default: none.

    useNativeEncoding

    flag to enable native encoding

    Default: false.

    The following properties are only available for Kafka Streams consumers and must be prefixed with spring.cloud.stream.kafka.streams.bindings.<binding name>.consumer.`literal. +For convenience, if there multiple input bindings and they all require a common value, that can be configured by using the prefix `spring.cloud.stream.kafka.streams.default.consumer..

    applicationId

    Setting application.id per input binding.

    Default: none

    keySerde

    key serde to use

    Default: none.

    valueSerde

    value serde to use

    Default: none.

    materializedAs

    state store to materialize when using incoming KTable types

    Default: none.

    useNativeDecoding

    flag to enable native decoding

    Default: false.

    dlqName

    DLQ topic name.

    Default: none.

    40.3.2 TimeWindow properties:

    Windowing is an important concept in stream processing applications. Following properties are available to configure +time-window computations.

    spring.cloud.stream.kafka.streams.timeWindow.length

    When this property is given, you can autowire a TimeWindows bean into the application. +The value is expressed in milliseconds.

    Default: none.

    spring.cloud.stream.kafka.streams.timeWindow.advanceBy

    Value is given in milliseconds.

    Default: none.

    40.4 Multiple Input Bindings

    For use cases that requires multiple incoming KStream objects or a combination of KStream and KTable objects, the Kafka +Streams binder provides multiple bindings support.

    Let’s see it in action.

    40.4.1 Multiple Input Bindings as a Sink

    @EnableBinding(KStreamKTableBinding.class)
    +.....
    +.....
    +@StreamListener
    +public void process(@Input("inputStream") KStream<String, PlayEvent> playEvents,
    +                    @Input("inputTable") KTable<Long, Song> songTable) {
    +                    ....
    +                    ....
    +}
    +
    +interface KStreamKTableBinding {
    +
    +    @Input("inputStream")
    +    KStream<?, ?> inputStream();
    +
    +    @Input("inputTable")
    +    KTable<?, ?> inputTable();
    +}

    In the above example, the application is written as a sink, i.e. there are no output bindings and the application has to +decide concerning downstream processing. When you write applications in this style, you might want to send the information +downstream or store them in a state store (See below for Queryable State Stores).

    In the case of incoming KTable, if you want to materialize the computations to a state store, you have to express it +through the following property.

    spring.cloud.stream.kafka.streams.bindings.inputTable.consumer.materializedAs: all-songs

    The above example shows the use of KTable as an input binding. +The binder also supports input bindings for GlobalKTable. +GlobalKTable binding is useful when you have to ensure that all instances of your application has access to the data updates from the topic. +KTable and GlobalKTable bindings are only available on the input. +Binder supports both input and output bindings for KStream.

    40.4.2 Multiple Input Bindings as a Processor

    @EnableBinding(KStreamKTableBinding.class)
    +....
    +....
    +
    +@StreamListener
    +@SendTo("output")
    +public KStream<String, Long> process(@Input("input") KStream<String, Long> userClicksStream,
    +                                     @Input("inputTable") KTable<String, String> userRegionsTable) {
    +....
    +....
    +}
    +
    +interface KStreamKTableBinding extends KafkaStreamsProcessor {
    +
    +    @Input("inputX")
    +    KTable<?, ?> inputTable();
    +}

    40.5 Multiple Output Bindings (aka Branching)

    Kafka Streams allow outbound data to be split into multiple topics based on some predicates. The Kafka Streams binder provides +support for this feature without compromising the programming model exposed through StreamListener in the end user application.

    You can write the application in the usual way as demonstrated above in the word count example. However, when using the +branching feature, you are required to do a few things. First, you need to make sure that your return type is KStream[] +instead of a regular KStream. Second, you need to use the SendTo annotation containing the output bindings in the order +(see example below). For each of these output bindings, you need to configure destination, content-type etc., complying with +the standard Spring Cloud Stream expectations.

    Here is an example:

    @EnableBinding(KStreamProcessorWithBranches.class)
    +@EnableAutoConfiguration
    +public static class WordCountProcessorApplication {
    +
    +    @Autowired
    +    private TimeWindows timeWindows;
    +
    +    @StreamListener("input")
    +    @SendTo({"output1","output2","output3})
    +    public KStream<?, WordCount>[] process(KStream<Object, String> input) {
    +
    +			Predicate<Object, WordCount> isEnglish = (k, v) -> v.word.equals("english");
    +			Predicate<Object, WordCount> isFrench =  (k, v) -> v.word.equals("french");
    +			Predicate<Object, WordCount> isSpanish = (k, v) -> v.word.equals("spanish");
    +
    +			return input
    +					.flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
    +					.groupBy((key, value) -> value)
    +					.windowedBy(timeWindows)
    +					.count(Materialized.as("WordCounts-1"))
    +					.toStream()
    +					.map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, new Date(key.window().start()), new Date(key.window().end()))))
    +					.branch(isEnglish, isFrench, isSpanish);
    +    }
    +
    +    interface KStreamProcessorWithBranches {
    +
    +    		@Input("input")
    +    		KStream<?, ?> input();
    +
    +    		@Output("output1")
    +    		KStream<?, ?> output1();
    +
    +    		@Output("output2")
    +    		KStream<?, ?> output2();
    +
    +    		@Output("output3")
    +    		KStream<?, ?> output3();
    +    	}
    +}

    Properties:

    spring.cloud.stream.bindings.output1.contentType: application/json
    +spring.cloud.stream.bindings.output2.contentType: application/json
    +spring.cloud.stream.bindings.output3.contentType: application/json
    +spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms: 1000
    +spring.cloud.stream.kafka.streams.binder.configuration:
    +  default.key.serde: org.apache.kafka.common.serialization.Serdes$StringSerde
    +  default.value.serde: org.apache.kafka.common.serialization.Serdes$StringSerde
    +spring.cloud.stream.bindings.output1:
    +  destination: foo
    +  producer:
    +    headerMode: raw
    +spring.cloud.stream.bindings.output2:
    +  destination: bar
    +  producer:
    +    headerMode: raw
    +spring.cloud.stream.bindings.output3:
    +  destination: fox
    +  producer:
    +    headerMode: raw
    +spring.cloud.stream.bindings.input:
    +  destination: words
    +  consumer:
    +    headerMode: raw

    40.6 Message Conversion

    Similar to message-channel based binder applications, the Kafka Streams binder adapts to the out-of-the-box content-type +conversions without any compromise.

    It is typical for Kafka Streams operations to know the type of SerDe’s used to transform the key and value correctly. +Therefore, it may be more natural to rely on the SerDe facilities provided by the Apache Kafka Streams library itself at +the inbound and outbound conversions rather than using the content-type conversions offered by the framework. +On the other hand, you might be already familiar with the content-type conversion patterns provided by the framework, and +that, you’d like to continue using for inbound and outbound conversions.

    Both the options are supported in the Kafka Streams binder implementation.

    40.6.1 Outbound serialization

    If native encoding is disabled (which is the default), then the framework will convert the message using the contentType +set by the user (otherwise, the default application/json will be applied). It will ignore any SerDe set on the outbound +in this case for outbound serialization.

    Here is the property to set the contentType on the outbound.

    spring.cloud.stream.bindings.output.contentType: application/json

    Here is the property to enable native encoding.

    spring.cloud.stream.bindings.output.nativeEncoding: true

    If native encoding is enabled on the output binding (user has to enable it as above explicitly), then the framework will +skip any form of automatic message conversion on the outbound. In that case, it will switch to the Serde set by the user. +The valueSerde property set on the actual output binding will be used. Here is an example.

    spring.cloud.stream.kafka.streams.bindings.output.producer.valueSerde: org.apache.kafka.common.serialization.Serdes$StringSerde

    If this property is not set, then it will use the "default" SerDe: spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde.

    It is worth to mention that Kafka Streams binder does not serialize the keys on outbound - it simply relies on Kafka itself. +Therefore, you either have to specify the keySerde property on the binding or it will default to the application-wide common +keySerde.

    Binding level key serde:

    spring.cloud.stream.kafka.streams.bindings.output.producer.keySerde

    Common Key serde:

    spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde

    If branching is used, then you need to use multiple output bindings. For example,

    interface KStreamProcessorWithBranches {
    +
    +    		@Input("input")
    +    		KStream<?, ?> input();
    +
    +    		@Output("output1")
    +    		KStream<?, ?> output1();
    +
    +    		@Output("output2")
    +    		KStream<?, ?> output2();
    +
    +    		@Output("output3")
    +    		KStream<?, ?> output3();
    +    	}

    If nativeEncoding is set, then you can set different SerDe’s on individual output bindings as below.

    spring.cloud.stream.kafka.streams.bindings.output1.producer.valueSerde=IntegerSerde
    +spring.cloud.stream.kafka.streams.bindings.output2.producer.valueSerde=StringSerde
    +spring.cloud.stream.kafka.streams.bindings.output3.producer.valueSerde=JsonSerde

    Then if you have SendTo like this, @SendTo({"output1", "output2", "output3"}), the KStream[] from the branches are +applied with proper SerDe objects as defined above. If you are not enabling nativeEncoding, you can then set different +contentType values on the output bindings as below. In that case, the framework will use the appropriate message converter +to convert the messages before sending to Kafka.

    spring.cloud.stream.bindings.output1.contentType: application/json
    +spring.cloud.stream.bindings.output2.contentType: application/java-serialzied-object
    +spring.cloud.stream.bindings.output3.contentType: application/octet-stream

    40.6.2 Inbound Deserialization

    Similar rules apply to data deserialization on the inbound.

    If native decoding is disabled (which is the default), then the framework will convert the message using the contentType +set by the user (otherwise, the default application/json will be applied). It will ignore any SerDe set on the inbound +in this case for inbound deserialization.

    Here is the property to set the contentType on the inbound.

    spring.cloud.stream.bindings.input.contentType: application/json

    Here is the property to enable native decoding.

    spring.cloud.stream.bindings.input.nativeDecoding: true

    If native decoding is enabled on the input binding (user has to enable it as above explicitly), then the framework will +skip doing any message conversion on the inbound. In that case, it will switch to the SerDe set by the user. The valueSerde +property set on the actual output binding will be used. Here is an example.

    spring.cloud.stream.kafka.streams.bindings.input.consumer.valueSerde: org.apache.kafka.common.serialization.Serdes$StringSerde

    If this property is not set, it will use the default SerDe: spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde.

    It is worth to mention that Kafka Streams binder does not deserialize the keys on inbound - it simply relies on Kafka itself. +Therefore, you either have to specify the keySerde property on the binding or it will default to the application-wide common +keySerde.

    Binding level key serde:

    spring.cloud.stream.kafka.streams.bindings.input.consumer.keySerde

    Common Key serde:

    spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde

    As in the case of KStream branching on the outbound, the benefit of setting value SerDe per binding is that if you have +multiple input bindings (multiple KStreams object) and they all require separate value SerDe’s, then you can configure +them individually. If you use the common configuration approach, then this feature won’t be applicable.

    40.7 Error Handling

    Apache Kafka Streams provide the capability for natively handling exceptions from deserialization errors. +For details on this support, please see this +Out of the box, Apache Kafka Streams provide two kinds of deserialization exception handlers - logAndContinue and logAndFail. +As the name indicates, the former will log the error and continue processing the next records and the latter will log the +error and fail. LogAndFail is the default deserialization exception handler.

    40.7.1 Handling Deserialization Exceptions

    Kafka Streams binder supports a selection of exception handlers through the following properties.

    spring.cloud.stream.kafka.streams.binder.serdeError: logAndContinue

    In addition to the above two deserialization exception handlers, the binder also provides a third one for sending the erroneous +records (poison pills) to a DLQ topic. Here is how you enable this DLQ exception handler.

    spring.cloud.stream.kafka.streams.binder.serdeError: sendToDlq

    When the above property is set, all the deserialization error records are automatically sent to the DLQ topic.

    spring.cloud.stream.kafka.streams.bindings.input.consumer.dlqName: foo-dlq

    If this is set, then the error records are sent to the topic foo-dlq. If this is not set, then it will create a DLQ +topic with the name error.<input-topic-name>.<group-name>.

    A couple of things to keep in mind when using the exception handling feature in Kafka Streams binder.

    • The property spring.cloud.stream.kafka.streams.binder.serdeError is applicable for the entire application. This implies +that if there are multiple StreamListener methods in the same application, this property is applied to all of them.
    • The exception handling for deserialization works consistently with native deserialization and framework provided message +conversion.

    40.7.2 Handling Non-Deserialization Exceptions

    For general error handling in Kafka Streams binder, it is up to the end user applications to handle application level errors. +As a side effect of providing a DLQ for deserialization exception handlers, Kafka Streams binder provides a way to get +access to the DLQ sending bean directly from your application. +Once you get access to that bean, you can programmatically send any exception records from your application to the DLQ.

    It continues to remain hard to robust error handling using the high-level DSL; Kafka Streams doesn’t natively support error +handling yet.

    However, when you use the low-level Processor API in your application, there are options to control this behavior. See +below.

    @Autowired
    +private SendToDlqAndContinue dlqHandler;
    +
    +@StreamListener("input")
    +@SendTo("output")
    +public KStream<?, WordCount> process(KStream<Object, String> input) {
    +
    +    input.process(() -> new Processor() {
    +    			ProcessorContext context;
    +
    +    			@Override
    +    			public void init(ProcessorContext context) {
    +    				this.context = context;
    +    			}
    +
    +    			@Override
    +    			public void process(Object o, Object o2) {
    +
    +    			    try {
    +    			        .....
    +    			        .....
    +    			    }
    +    			    catch(Exception e) {
    +    			        //explicitly provide the kafka topic corresponding to the input binding as the first argument.
    +                        //DLQ handler will correctly map to the dlq topic from the actual incoming destination.
    +                        dlqHandler.sendToDlq("topic-name", (byte[]) o1, (byte[]) o2, context.partition());
    +    			    }
    +    			}
    +
    +    			.....
    +    			.....
    +    });
    +}

    40.8 State Store

    State store is created automatically by Kafka Streams when the DSL is used. +When processor API is used, you need to register a state store manually. In order to do so, you can use KafkaStreamsStateStore annotation. +You can specify the name and type of the store, flags to control log and disabling cache, etc. +Once the store is created by the binder during the bootstrapping phase, you can access this state store through the processor API. +Below are some primitives for doing this.

    Creating a state store:

    @KafkaStreamsStateStore(name="mystate", type= KafkaStreamsStateStoreProperties.StoreType.WINDOW, lengthMs=300000)
    +public void process(KStream<Object, Product> input) {
    +    ...
    +}

    Accessing the state store:

    Processor<Object, Product>() {
    +
    +    WindowStore<Object, String> state;
    +
    +    @Override
    +    public void init(ProcessorContext processorContext) {
    +        state = (WindowStore)processorContext.getStateStore("mystate");
    +    }
    +    ...
    +}

    40.9 Interactive Queries

    As part of the public Kafka Streams binder API, we expose a class called InteractiveQueryService. +You can access this as a Spring bean in your application. An easy way to get access to this bean from your application is to "autowire" the bean.

    @Autowired
    +private InteractiveQueryService interactiveQueryService;

    Once you gain access to this bean, then you can query for the particular state-store that you are interested. See below.

    ReadOnlyKeyValueStore<Object, Object> keyValueStore =
    +						interactiveQueryService.getQueryableStoreType("my-store", QueryableStoreTypes.keyValueStore());

    If there are multiple instances of the kafka streams application running, then before you can query them interactively, you need to identify which application instance hosts the key. +InteractiveQueryService API provides methods for identifying the host information.

    In order for this to work, you must configure the property application.server as below:

    spring.cloud.stream.kafka.streams.binder.configuration.application.server: <server>:<port>

    Here are some code snippets:

    org.apache.kafka.streams.state.HostInfo hostInfo = interactiveQueryService.getHostInfo("store-name",
    +						key, keySerializer);
    +
    +if (interactiveQueryService.getCurrentHostInfo().equals(hostInfo)) {
    +
    +    //query from the store that is locally available
    +}
    +else {
    +    //query from the remote host
    +}

    40.10 Accessing the underlying KafkaStreams object

    StreamBuilderFactoryBean from spring-kafka that is responsible for constructing the KafkaStreams object can be accessed programmatically. +Each StreamBuilderFactoryBean is registered as stream-builder and appended with the StreamListener method name. +If your StreamListener method is named as process for example, the stream builder bean is named as stream-builder-process. +Since this is a factory bean, it should be accessed by prepending an ampersand (&) when accessing it programmatically. +Following is an example and it assumes the StreamListener method is named as process

    StreamsBuilderFactoryBean streamsBuilderFactoryBean = context.getBean("&stream-builder-process", StreamsBuilderFactoryBean.class);
    +			KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams();

    40.11 State Cleanup

    By default, the Kafkastreams.cleanup() method is called when the binding is stopped. +See the Spring Kafka documentation. +To modify this behavior simply add a single CleanupConfig @Bean (configured to clean up on start, stop, or neither) to the application context; the bean will be detected and wired into the factory bean.

    41. RabbitMQ Binder

    41.1 Usage

    To use the RabbitMQ binder, you can add it to your Spring Cloud Stream application, by using the following Maven coordinates:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    +</dependency>

    Alternatively, you can use the Spring Cloud Stream RabbitMQ Starter, as follows:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    +</dependency>

    41.2 RabbitMQ Binder Overview

    The following simplified diagram shows how the RabbitMQ binder operates:

    Figure 41.1. RabbitMQ Binder

    rabbit binder

    By default, the RabbitMQ Binder implementation maps each destination to a TopicExchange. +For each consumer group, a Queue is bound to that TopicExchange. +Each consumer instance has a corresponding RabbitMQ Consumer instance for its group’s Queue. +For partitioned producers and consumers, the queues are suffixed with the partition index and use the partition index as the routing key. +For anonymous consumers (those with no group property), an auto-delete queue (with a randomized unique name) is used.

    By using the optional autoBindDlq option, you can configure the binder to create and configure dead-letter queues (DLQs) (and a dead-letter exchange DLX, as well as routing infrastructure). +By default, the dead letter queue has the name of the destination, appended with .dlq. +If retry is enabled (maxAttempts > 1), failed messages are delivered to the DLQ after retries are exhausted. +If retry is disabled (maxAttempts = 1), you should set requeueRejected to false (the default) so that failed messages are routed to the DLQ, instead of being re-queued. +In addition, republishToDlq causes the binder to publish a failed message to the DLQ (instead of rejecting it). +This feature lets additional information (such as the stack trace in the x-exception-stacktrace header) be added to the message in headers. +This option does not need retry enabled. +You can republish a failed message after just one attempt. +Starting with version 1.2, you can configure the delivery mode of republished messages. +See the republishDeliveryMode property.

    [Important]Important

    Setting requeueRejected to true (with republishToDlq=false ) causes the message to be re-queued and redelivered continually, which is likely not what you want unless the reason for the failure is transient. +In general, you should enable retry within the binder by setting maxAttempts to greater than one or by setting republishToDlq to true.

    See Section 41.3.1, “RabbitMQ Binder Properties” for more information about these properties.

    The framework does not provide any standard mechanism to consume dead-letter messages (or to re-route them back to the primary queue). +Some options are described in Section 41.6, “Dead-Letter Queue Processing”.

    [Note]Note

    When multiple RabbitMQ binders are used in a Spring Cloud Stream application, it is important to disable 'RabbitAutoConfiguration' to avoid the same configuration from RabbitAutoConfiguration being applied to the two binders. +You can exclude the class by using the @SpringBootApplication annotation.

    Starting with version 2.0, the RabbitMessageChannelBinder sets the RabbitTemplate.userPublisherConnection property to true so that the non-transactional producers avoid deadlocks on consumers, which can happen if cached connections are blocked because of a memory alarm on the broker.

    [Note]Note

    Currently, a multiplex consumer (a single consumer listening to multiple queues) is only supported for message-driven conssumers; polled consumers can only retrieve messages from a single queue.

    41.3 Configuration Options

    This section contains settings specific to the RabbitMQ Binder and bound channels.

    For general binding configuration options and properties, see the Spring Cloud Stream core documentation.

    41.3.1 RabbitMQ Binder Properties

    By default, the RabbitMQ binder uses Spring Boot’s ConnectionFactory. +Conseuqently, it supports all Spring Boot configuration options for RabbitMQ. +(For reference, see the Spring Boot documentation). +RabbitMQ configuration options use the spring.rabbitmq prefix.

    In addition to Spring Boot options, the RabbitMQ binder supports the following properties:

    spring.cloud.stream.rabbit.binder.adminAddresses

    A comma-separated list of RabbitMQ management plugin URLs. +Only used when nodes contains more than one entry. +Each entry in this list must have a corresponding entry in spring.rabbitmq.addresses. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See Queue Affinity and the LocalizedQueueConnectionFactory for more information.

    Default: empty.

    spring.cloud.stream.rabbit.binder.nodes

    A comma-separated list of RabbitMQ node names. +When more than one entry, used to locate the server address where a queue is located. +Each entry in this list must have a corresponding entry in spring.rabbitmq.addresses. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See Queue Affinity and the LocalizedQueueConnectionFactory for more information.

    Default: empty.

    spring.cloud.stream.rabbit.binder.compressionLevel

    The compression level for compressed bindings. +See java.util.zip.Deflater.

    Default: 1 (BEST_LEVEL).

    spring.cloud.stream.binder.connection-name-prefix

    A connection name prefix used to name the connection(s) created by this binder. +The name is this prefix followed by #n, where n increments each time a new connection is opened.

    Default: none (Spring AMQP default).

    41.3.2 RabbitMQ Consumer Properties

    The following properties are available for Rabbit consumers only and must be prefixed with spring.cloud.stream.rabbit.bindings.<channelName>.consumer..

    acknowledgeMode

    The acknowledge mode.

    Default: AUTO.

    autoBindDlq

    Whether to automatically declare the DLQ and bind it to the binder DLX.

    Default: false.

    bindingRoutingKey

    The routing key with which to bind the queue to the exchange (if bindQueue is true). +For partitioned destinations, -<instanceIndex> is appended.

    Default: #.

    bindQueue

    Whether to bind the queue to the destination exchange. +Set it to false if you have set up your own infrastructure and have previously created and bound the queue.

    Default: true.

    consumerTagPrefix

    Used to create the consumer tag(s); will be appended by #n where n increments for each consumer created. +Example: ${spring.application.name}-${spring.cloud.stream.bindings.input.group}-${spring.cloud.stream.instance-index}.

    Default: none - the broker will generate random consumer tags.

    deadLetterQueueName

    The name of the DLQ

    Default: prefix+destination.dlq

    deadLetterExchange

    A DLX to assign to the queue. +Relevant only if autoBindDlq is true.

    Default: 'prefix+DLX'

    deadLetterExchangeType

    The type of the DLX to assign to the queue. +Relevant only if autoBindDlq is true.

    Default: 'direct'

    deadLetterRoutingKey

    A dead letter routing key to assign to the queue. +Relevant only if autoBindDlq is true.

    Default: destination

    declareDlx

    Whether to declare the dead letter exchange for the destination. +Relevant only if autoBindDlq is true. +Set to false if you have a pre-configured DLX.

    Default: true.

    declareExchange

    Whether to declare the exchange for the destination.

    Default: true.

    delayedExchange

    Whether to declare the exchange as a Delayed Message Exchange. +Requires the delayed message exchange plugin on the broker. +The x-delayed-type argument is set to the exchangeType.

    Default: false.

    dlqDeadLetterExchange

    If a DLQ is declared, a DLX to assign to that queue.

    Default: none

    dlqDeadLetterRoutingKey

    If a DLQ is declared, a dead letter routing key to assign to that queue.

    Default: none

    dlqExpires

    How long before an unused dead letter queue is deleted (in milliseconds).

    Default: no expiration

    dlqLazy

    Declare the dead letter queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue.

    Default: false.

    dlqMaxLength

    Maximum number of messages in the dead letter queue.

    Default: no limit

    dlqMaxLengthBytes

    Maximum number of total bytes in the dead letter queue from all messages.

    Default: no limit

    dlqMaxPriority

    Maximum priority of messages in the dead letter queue (0-255).

    Default: none

    dlqOverflowBehavior

    Action to take when dlqMaxLength or dlqMaxLengthBytes is exceeded; currently drop-head or reject-publish but refer to the RabbitMQ documentation.

    Default: none

    dlqTtl

    Default time to live to apply to the dead letter queue when declared (in milliseconds).

    Default: no limit

    durableSubscription

    Whether the subscription should be durable. +Only effective if group is also set.

    Default: true.

    exchangeAutoDelete

    If declareExchange is true, whether the exchange should be auto-deleted (that is, removed after the last queue is removed).

    Default: true.

    exchangeDurable

    If declareExchange is true, whether the exchange should be durable (that is, it survives broker restart).

    Default: true.

    exchangeType

    The exchange type: direct, fanout or topic for non-partitioned destinations and direct or topic for partitioned destinations.

    Default: topic.

    exclusive

    Whether to create an exclusive consumer. +Concurrency should be 1 when this is true. +Often used when strict ordering is required but enabling a hot standby instance to take over after a failure. +See recoveryInterval, which controls how often a standby instance attempts to consume.

    Default: false.

    expires

    How long before an unused queue is deleted (in milliseconds).

    Default: no expiration

    failedDeclarationRetryInterval

    The interval (in milliseconds) between attempts to consume from a queue if it is missing.

    Default: 5000

    headerPatterns

    Patterns for headers to be mapped from inbound messages.

    Default: ['*'] (all headers).

    lazy

    Declare the queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue.

    Default: false.

    maxConcurrency

    The maximum number of consumers.

    Default: 1.

    maxLength

    The maximum number of messages in the queue.

    Default: no limit

    maxLengthBytes

    The maximum number of total bytes in the queue from all messages.

    Default: no limit

    maxPriority

    The maximum priority of messages in the queue (0-255).

    Default: none

    missingQueuesFatal

    When the queue cannot be found, whether to treat the condition as fatal and stop the listener container. +Defaults to false so that the container keeps trying to consume from the queue — for example, when using a cluster and the node hosting a non-HA queue is down.

    Default: false

    overflowBehavior

    Action to take when maxLength or maxLengthBytes is exceeded; currently drop-head or reject-publish but refer to the RabbitMQ documentation.

    Default: none

    prefetch

    Prefetch count.

    Default: 1.

    prefix

    A prefix to be added to the name of the destination and queues.

    Default: "".

    queueDeclarationRetries

    The number of times to retry consuming from a queue if it is missing. +Relevant only when missingQueuesFatal is true. +Otherwise, the container keeps retrying indefinitely.

    Default: 3

    queueNameGroupOnly

    When true, consume from a queue with a name equal to the group. +Otherwise the queue name is destination.group. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue.

    Default: false.

    recoveryInterval

    The interval between connection recovery attempts, in milliseconds.

    Default: 5000.

    requeueRejected

    Whether delivery failures should be re-queued when retry is disabled or republishToDlq is false.

    Default: false.

    republishDeliveryMode

    When republishToDlq is true, specifies the delivery mode of the republished message.

    Default: DeliveryMode.PERSISTENT

    republishToDlq

    By default, messages that fail after retries are exhausted are rejected. +If a dead-letter queue (DLQ) is configured, RabbitMQ routes the failed message (unchanged) to the DLQ. +If set to true, the binder republishs failed messages to the DLQ with additional headers, including the exception message and stack trace from the cause of the final failure.

    Default: false

    transacted

    Whether to use transacted channels.

    Default: false.

    ttl

    Default time to live to apply to the queue when declared (in milliseconds).

    Default: no limit

    txSize

    The number of deliveries between acks.

    Default: 1.

    41.3.3 Advanced Listener Container Configuration

    To set listener container properties that are not exposed as binder or binding properties, add a single bean of type ListenerContainerCustomizer to the application context. +The binder and binding properties will be set and then the customizer will be called. +The customizer (configure() method) is provided with the queue name as well as the consumer group as arguments.

    41.3.4 Rabbit Producer Properties

    The following properties are available for Rabbit producers only and +must be prefixed with spring.cloud.stream.rabbit.bindings.<channelName>.producer..

    autoBindDlq

    Whether to automatically declare the DLQ and bind it to the binder DLX.

    Default: false.

    batchingEnabled

    Whether to enable message batching by producers. +Messages are batched into one message according to the following properties (described in the next three entries in this list): 'batchSize', batchBufferLimit, and batchTimeout. +See Batching for more information.

    Default: false.

    batchSize

    The number of messages to buffer when batching is enabled.

    Default: 100.

    batchBufferLimit

    The maximum buffer size when batching is enabled.

    Default: 10000.

    batchTimeout

    The batch timeout when batching is enabled.

    Default: 5000.

    bindingRoutingKey

    The routing key with which to bind the queue to the exchange (if bindQueue is true). +Only applies to non-partitioned destinations. +Only applies if requiredGroups are provided and then only to those groups.

    Default: #.

    bindQueue

    Whether to bind the queue to the destination exchange. +Set it to false if you have set up your own infrastructure and have previously created and bound the queue. +Only applies if requiredGroups are provided and then only to those groups.

    Default: true.

    compress

    Whether data should be compressed when sent.

    Default: false.

    deadLetterQueueName

    The name of the DLQ +Only applies if requiredGroups are provided and then only to those groups.

    Default: prefix+destination.dlq

    deadLetterExchange

    A DLX to assign to the queue. +Relevant only when autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups.

    Default: 'prefix+DLX'

    deadLetterExchangeType

    The type of the DLX to assign to the queue. +Relevant only if autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups.

    Default: 'direct'

    deadLetterRoutingKey

    A dead letter routing key to assign to the queue. +Relevant only when autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups.

    Default: destination

    declareDlx

    Whether to declare the dead letter exchange for the destination. +Relevant only if autoBindDlq is true. +Set to false if you have a pre-configured DLX. +Applies only when requiredGroups are provided and then only to those groups.

    Default: true.

    declareExchange

    Whether to declare the exchange for the destination.

    Default: true.

    delayExpression

    A SpEL expression to evaluate the delay to apply to the message (x-delay header). +It has no effect if the exchange is not a delayed message exchange.

    Default: No x-delay header is set.

    delayedExchange

    Whether to declare the exchange as a Delayed Message Exchange. +Requires the delayed message exchange plugin on the broker. +The x-delayed-type argument is set to the exchangeType.

    Default: false.

    deliveryMode

    The delivery mode.

    Default: PERSISTENT.

    dlqDeadLetterExchange

    When a DLQ is declared, a DLX to assign to that queue. +Applies only if requiredGroups are provided and then only to those groups.

    Default: none

    dlqDeadLetterRoutingKey

    When a DLQ is declared, a dead letter routing key to assign to that queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: none

    dlqExpires

    How long (in milliseconds) before an unused dead letter queue is deleted. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no expiration

    dlqLazy
    Declare the dead letter queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when requiredGroups are provided and then only to those groups.
    dlqMaxLength

    Maximum number of messages in the dead letter queue. +Applies only if requiredGroups are provided and then only to those groups.

    Default: no limit

    dlqMaxLengthBytes

    Maximum number of total bytes in the dead letter queue from all messages. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    dlqMaxPriority

    Maximum priority of messages in the dead letter queue (0-255) +Applies only when requiredGroups are provided and then only to those groups.

    Default: none

    dlqTtl

    Default time (in milliseconds) to live to apply to the dead letter queue when declared. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    exchangeAutoDelete

    If declareExchange is true, whether the exchange should be auto-delete (it is removed after the last queue is removed).

    Default: true.

    exchangeDurable

    If declareExchange is true, whether the exchange should be durable (survives broker restart).

    Default: true.

    exchangeType

    The exchange type: direct, fanout or topic for non-partitioned destinations and direct or topic for partitioned destinations.

    Default: topic.

    expires

    How long (in milliseconds) before an unused queue is deleted. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no expiration

    headerPatterns

    Patterns for headers to be mapped to outbound messages.

    Default: ['*'] (all headers).

    lazy

    Declare the queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: false.

    maxLength

    Maximum number of messages in the queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    maxLengthBytes

    Maximum number of total bytes in the queue from all messages. +Only applies if requiredGroups are provided and then only to those groups.

    Default: no limit

    maxPriority

    Maximum priority of messages in the queue (0-255). +Only applies if requiredGroups are provided and then only to those groups.

    Default: none

    prefix

    A prefix to be added to the name of the destination exchange.

    Default: "".

    queueNameGroupOnly

    When true, consume from a queue with a name equal to the group. +Otherwise the queue name is destination.group. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. +Applies only when requiredGroups are provided and then only to those groups.

    Default: false.

    routingKeyExpression

    A SpEL expression to determine the routing key to use when publishing messages. +For a fixed routing key, use a literal expression, such as routingKeyExpression='my.routingKey' in a properties file or routingKeyExpression: '''my.routingKey''' in a YAML file.

    Default: destination or destination-<partition> for partitioned destinations.

    transacted

    Whether to use transacted channels.

    Default: false.

    ttl

    Default time (in milliseconds) to live to apply to the queue when declared. +Applies only when requiredGroups are provided and then only to those groups.

    Default: no limit

    [Note]Note

    In the case of RabbitMQ, content type headers can be set by external applications. +Spring Cloud Stream supports them as part of an extended internal protocol used for any type of transport — including transports, such as Kafka (prior to 0.11), that do not natively support headers.

    41.4 Retry With the RabbitMQ Binder

    When retry is enabled within the binder, the listener container thread is suspended for any back off periods that are configured. +This might be important when strict ordering is required with a single consumer. However, for other use cases, it prevents other messages from being processed on that thread. +An alternative to using binder retry is to set up dead lettering with time to live on the dead-letter queue (DLQ) as well as dead-letter configuration on the DLQ itself. +See Section 41.3.1, “RabbitMQ Binder Properties” for more information about the properties discussed here. +You can use the following example configuration to enable this feature:

    • Set autoBindDlq to true. +The binder create a DLQ. +Optionally, you can specify a name in deadLetterQueueName.
    • Set dlqTtl to the back off time you want to wait between redeliveries.
    • Set the dlqDeadLetterExchange to the default exchange. +Expired messages from the DLQ are routed to the original queue, because the default deadLetterRoutingKey is the queue name (destination.group). +Setting to the default exchange is achieved by setting the property with no value, as shown in the next example.

    To force a message to be dead-lettered, either throw an AmqpRejectAndDontRequeueException or set requeueRejected to true (the default) and throw any exception.

    The loop continue without end, which is fine for transient problems, but you may want to give up after some number of attempts. +Fortunately, RabbitMQ provides the x-death header, which lets you determine how many cycles have occurred.

    To acknowledge a message after giving up, throw an ImmediateAcknowledgeAmqpException.

    41.4.1 Putting it All Together

    The following configuration creates an exchange myDestination with queue myDestination.consumerGroup bound to a topic exchange with a wildcard routing key #:

    ---
    +spring.cloud.stream.bindings.input.destination=myDestination
    +spring.cloud.stream.bindings.input.group=consumerGroup
    +#disable binder retries
    +spring.cloud.stream.bindings.input.consumer.max-attempts=1
    +#dlx/dlq setup
    +spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true
    +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-ttl=5000
    +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-dead-letter-exchange=
    +---

    This configuration creates a DLQ bound to a direct exchange (DLX) with a routing key of myDestination.consumerGroup. +When messages are rejected, they are routed to the DLQ. +After 5 seconds, the message expires and is routed to the original queue by using the queue name as the routing key, as shown in the following example:

    Spring Boot application.  +

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class XDeathApplication {
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(XDeathApplication.class, args);
    +    }
    +
    +    @StreamListener(Sink.INPUT)
    +    public void listen(String in, @Header(name = "x-death", required = false) Map<?,?> death) {
    +        if (death != null && death.get("count").equals(3L)) {
    +            // giving up - don't send to DLX
    +            throw new ImmediateAcknowledgeAmqpException("Failed after 4 attempts");
    +        }
    +        throw new AmqpRejectAndDontRequeueException("failed");
    +    }
    +
    +}

    +

    Notice that the count property in the x-death header is a Long.

    41.5 Error Channels

    Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See ??? for more information.

    RabbitMQ has two types of send failures:

    The latter is rare. +According to the RabbitMQ documentation "[A nack] will only be delivered if an internal error occurs in the Erlang process responsible for a queue.".

    As well as enabling producer error channels (as described in ???), the RabbitMQ binder only sends messages to the channels if the connection factory is appropriately configured, as follows:

    • ccf.setPublisherConfirms(true);
    • ccf.setPublisherReturns(true);

    When using Spring Boot configuration for the connection factory, set the following properties:

    • spring.rabbitmq.publisher-confirms
    • spring.rabbitmq.publisher-returns

    The payload of the ErrorMessage for a returned message is a ReturnedAmqpMessageException with the following properties:

    • failedMessage: The spring-messaging Message<?> that failed to be sent.
    • amqpMessage: The raw spring-amqp Message.
    • replyCode: An integer value indicating the reason for the failure (for example, 312 - No route).
    • replyText: A text value indicating the reason for the failure (for example, NO_ROUTE).
    • exchange: The exchange to which the message was published.
    • routingKey: The routing key used when the message was published.

    For negatively acknowledged confirmations, the payload is a NackedAmqpMessageException with the following properties:

    • failedMessage: The spring-messaging Message<?> that failed to be sent.
    • nackReason: A reason (if available — you may need to examine the broker logs for more information).

    There is no automatic handling of these exceptions (such as sending to a dead-letter queue). +You can consume these exceptions with your own Spring Integration flow.

    41.6 Dead-Letter Queue Processing

    Because you cannot anticipate how users would want to dispose of dead-lettered messages, the framework does not provide any standard mechanism to handle them. +If the reason for the dead-lettering is transient, you may wish to route the messages back to the original queue. +However, if the problem is a permanent issue, that could cause an infinite loop. +The following Spring Boot application shows an example of how to route those messages back to the original queue but moves them to a third parking lot queue after three attempts. +The second example uses the RabbitMQ Delayed Message Exchange to introduce a delay to the re-queued message. +In this example, the delay increases for each attempt. +These examples use a @RabbitListener to receive messages from the DLQ. +You could also use RabbitTemplate.receive() in a batch process.

    The examples assume the original destination is so8400in and the consumer group is so8400.

    41.6.1 Non-Partitioned Destinations

    The first two examples are for when the destination is not partitioned:

    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +    private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +    private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +    private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +    private static final String X_RETRIES_HEADER = "x-retries";
    +
    +    public static void main(String[] args) throws Exception {
    +        ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +        System.out.println("Hit enter to terminate");
    +        System.in.read();
    +        context.close();
    +    }
    +
    +    @Autowired
    +    private RabbitTemplate rabbitTemplate;
    +
    +    @RabbitListener(queues = DLQ)
    +    public void rePublish(Message failedMessage) {
    +        Integer retriesHeader = (Integer) failedMessage.getMessageProperties().getHeaders().get(X_RETRIES_HEADER);
    +        if (retriesHeader == null) {
    +            retriesHeader = Integer.valueOf(0);
    +        }
    +        if (retriesHeader < 3) {
    +            failedMessage.getMessageProperties().getHeaders().put(X_RETRIES_HEADER, retriesHeader + 1);
    +            this.rabbitTemplate.send(ORIGINAL_QUEUE, failedMessage);
    +        }
    +        else {
    +            this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +        }
    +    }
    +
    +    @Bean
    +    public Queue parkingLot() {
    +        return new Queue(PARKING_LOT);
    +    }
    +
    +}
    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +    private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +    private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +    private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +    private static final String X_RETRIES_HEADER = "x-retries";
    +
    +    private static final String DELAY_EXCHANGE = "dlqReRouter";
    +
    +    public static void main(String[] args) throws Exception {
    +        ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +        System.out.println("Hit enter to terminate");
    +        System.in.read();
    +        context.close();
    +    }
    +
    +    @Autowired
    +    private RabbitTemplate rabbitTemplate;
    +
    +    @RabbitListener(queues = DLQ)
    +    public void rePublish(Message failedMessage) {
    +        Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders();
    +        Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER);
    +        if (retriesHeader == null) {
    +            retriesHeader = Integer.valueOf(0);
    +        }
    +        if (retriesHeader < 3) {
    +            headers.put(X_RETRIES_HEADER, retriesHeader + 1);
    +            headers.put("x-delay", 5000 * retriesHeader);
    +            this.rabbitTemplate.send(DELAY_EXCHANGE, ORIGINAL_QUEUE, failedMessage);
    +        }
    +        else {
    +            this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +        }
    +    }
    +
    +    @Bean
    +    public DirectExchange delayExchange() {
    +        DirectExchange exchange = new DirectExchange(DELAY_EXCHANGE);
    +        exchange.setDelayed(true);
    +        return exchange;
    +    }
    +
    +    @Bean
    +    public Binding bindOriginalToDelay() {
    +        return BindingBuilder.bind(new Queue(ORIGINAL_QUEUE)).to(delayExchange()).with(ORIGINAL_QUEUE);
    +    }
    +
    +    @Bean
    +    public Queue parkingLot() {
    +        return new Queue(PARKING_LOT);
    +    }
    +
    +}

    41.6.2 Partitioned Destinations

    With partitioned destinations, there is one DLQ for all partitions. We determine the original queue from the headers.

    republishToDlq=false

    When republishToDlq is false, RabbitMQ publishes the message to the DLX/DLQ with an x-death header containing information about the original destination, as shown in the following example:

    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +	private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +	private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +	private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +	private static final String X_DEATH_HEADER = "x-death";
    +
    +	private static final String X_RETRIES_HEADER = "x-retries";
    +
    +	public static void main(String[] args) throws Exception {
    +		ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +		System.out.println("Hit enter to terminate");
    +		System.in.read();
    +		context.close();
    +	}
    +
    +	@Autowired
    +	private RabbitTemplate rabbitTemplate;
    +
    +	@SuppressWarnings("unchecked")
    +	@RabbitListener(queues = DLQ)
    +	public void rePublish(Message failedMessage) {
    +		Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders();
    +		Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER);
    +		if (retriesHeader == null) {
    +			retriesHeader = Integer.valueOf(0);
    +		}
    +		if (retriesHeader < 3) {
    +			headers.put(X_RETRIES_HEADER, retriesHeader + 1);
    +			List<Map<String, ?>> xDeath = (List<Map<String, ?>>) headers.get(X_DEATH_HEADER);
    +			String exchange = (String) xDeath.get(0).get("exchange");
    +			List<String> routingKeys = (List<String>) xDeath.get(0).get("routing-keys");
    +			this.rabbitTemplate.send(exchange, routingKeys.get(0), failedMessage);
    +		}
    +		else {
    +			this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +		}
    +	}
    +
    +	@Bean
    +	public Queue parkingLot() {
    +		return new Queue(PARKING_LOT);
    +	}
    +
    +}

    republishToDlq=true

    When republishToDlq is true, the republishing recoverer adds the original exchange and routing key to headers, as shown in the following example:

    @SpringBootApplication
    +public class ReRouteDlqApplication {
    +
    +	private static final String ORIGINAL_QUEUE = "so8400in.so8400";
    +
    +	private static final String DLQ = ORIGINAL_QUEUE + ".dlq";
    +
    +	private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot";
    +
    +	private static final String X_RETRIES_HEADER = "x-retries";
    +
    +	private static final String X_ORIGINAL_EXCHANGE_HEADER = RepublishMessageRecoverer.X_ORIGINAL_EXCHANGE;
    +
    +	private static final String X_ORIGINAL_ROUTING_KEY_HEADER = RepublishMessageRecoverer.X_ORIGINAL_ROUTING_KEY;
    +
    +	public static void main(String[] args) throws Exception {
    +		ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args);
    +		System.out.println("Hit enter to terminate");
    +		System.in.read();
    +		context.close();
    +	}
    +
    +	@Autowired
    +	private RabbitTemplate rabbitTemplate;
    +
    +	@RabbitListener(queues = DLQ)
    +	public void rePublish(Message failedMessage) {
    +		Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders();
    +		Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER);
    +		if (retriesHeader == null) {
    +			retriesHeader = Integer.valueOf(0);
    +		}
    +		if (retriesHeader < 3) {
    +			headers.put(X_RETRIES_HEADER, retriesHeader + 1);
    +			String exchange = (String) headers.get(X_ORIGINAL_EXCHANGE_HEADER);
    +			String originalRoutingKey = (String) headers.get(X_ORIGINAL_ROUTING_KEY_HEADER);
    +			this.rabbitTemplate.send(exchange, originalRoutingKey, failedMessage);
    +		}
    +		else {
    +			this.rabbitTemplate.send(PARKING_LOT, failedMessage);
    +		}
    +	}
    +
    +	@Bean
    +	public Queue parkingLot() {
    +		return new Queue(PARKING_LOT);
    +	}
    +
    +}

    41.7 Partitioning with the RabbitMQ Binder

    RabbitMQ does not support partitioning natively.

    Sometimes, it is advantageous to send data to specific partitions — for example, when you want to strictly order message processing, all messages for a particular customer should go to the same partition.

    The RabbitMessageChannelBinder provides partitioning by binding a queue for each partition to the destination exchange.

    The following Java and YAML examples show how to configure the producer:

    Producer.  +

    @SpringBootApplication
    +@EnableBinding(Source.class)
    +public class RabbitPartitionProducerApplication {
    +
    +    private static final Random RANDOM = new Random(System.currentTimeMillis());
    +
    +    private static final String[] data = new String[] {
    +            "abc1", "def1", "qux1",
    +            "abc2", "def2", "qux2",
    +            "abc3", "def3", "qux3",
    +            "abc4", "def4", "qux4",
    +            };
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(RabbitPartitionProducerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedRate = "5000"))
    +    public Message<?> generate() {
    +        String value = data[RANDOM.nextInt(data.length)];
    +        System.out.println("Sending: " + value);
    +        return MessageBuilder.withPayload(value)
    +                .setHeader("partitionKey", value)
    +                .build();
    +    }
    +
    +}

    +

    application.yml.  +

        spring:
    +      cloud:
    +        stream:
    +          bindings:
    +            output:
    +              destination: partitioned.destination
    +              producer:
    +                partitioned: true
    +                partition-key-expression: headers['partitionKey']
    +                partition-count: 2
    +                required-groups:
    +                - myGroup

    +

    [Note]Note

    The configuration in the prececing example uses the default partitioning (key.hashCode() % partitionCount). +This may or may not provide a suitably balanced algorithm, depending on the key values. +You can override this default by using the partitionSelectorExpression or partitionSelectorClass properties.

    The required-groups property is required only if you need the consumer queues to be provisioned when the producer is deployed. +Otherwise, any messages sent to a partition are lost until the corresponding consumer is deployed.

    The following configuration provisions a topic exchange:

    part exchange

    The following queues are bound to that exchange:

    part queues

    The following bindings associate the queues to the exchange:

    part bindings

    The following Java and YAML examples continue the previous examples and show how to configure the consumer:

    Consumer.  +

    @SpringBootApplication
    +@EnableBinding(Sink.class)
    +public class RabbitPartitionConsumerApplication {
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(RabbitPartitionConsumerApplication.class)
    +            .web(false)
    +            .run(args);
    +    }
    +
    +    @StreamListener(Sink.INPUT)
    +    public void listen(@Payload String in, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) {
    +        System.out.println(in + " received from queue " + queue);
    +    }
    +
    +}

    +

    application.yml.  +

        spring:
    +      cloud:
    +        stream:
    +          bindings:
    +            input:
    +              destination: partitioned.destination
    +              group: myGroup
    +              consumer:
    +                partitioned: true
    +                instance-index: 0

    +

    [Important]Important

    The RabbitMessageChannelBinder does not support dynamic scaling. +There must be at least one consumer per partition. +The consumer’s instanceIndex is used to indicate which partition is consumed. +Platforms such as Cloud Foundry can have only one instance with an instanceIndex.

    Part VII. Spring Cloud Bus

    Spring Cloud Bus links the nodes of a distributed system with a lightweight message +broker. This broker can then be used to broadcast state changes (such as configuration +changes) or other management instructions. A key idea is that the bus is like a +distributed actuator for a Spring Boot application that is scaled out. However, it can +also be used as a communication channel between apps. This project provides starters for +either an AMQP broker or Kafka as the transport.

    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github.

    42. Quick Start

    Spring Cloud Bus works by adding Spring Boot autconfiguration if it detects itself on the +classpath. To enable the bus, add spring-cloud-starter-bus-amqp or +spring-cloud-starter-bus-kafka to your dependency management. Spring Cloud takes care of +the rest. Make sure the broker (RabbitMQ or Kafka) is available and configured. When +running on localhost, you need not do anything. If you run remotely, use Spring Cloud +Connectors or Spring Boot conventions to define the broker credentials, as shown in the +following example for Rabbit:

    application.yml.  +

    spring:
    +  rabbitmq:
    +    host: mybroker.com
    +    port: 5672
    +    username: user
    +    password: secret

    +

    The bus currently supports sending messages to all nodes listening or all nodes for a +particular service (as defined by Eureka). The /bus/* actuator namespace has some HTTP +endpoints. Currently, two are implemented. The first, /bus/env, sends key/value pairs to +update each node’s Spring Environment. The second, /bus/refresh, reloads each +application’s configuration, as though they had all been pinged on their /refresh +endpoint.

    [Note]Note

    The Spring Cloud Bus starters cover Rabbit and Kafka, because those are the two most +common implementations. However, Spring Cloud Stream is quite flexible, and the binder +works with spring-cloud-bus.

    43. Bus Endpoints

    Spring Cloud Bus provides two endpoints, /actuator/bus-refresh and /actuator/bus-env +that correspond to individual actuator endpoints in Spring Cloud Commons, +/actuator/refresh and /actuator/env respectively.

    43.1 Bus Refresh Endpoint

    The /actuator/bus-refresh endpoint clears the RefreshScope cache and rebinds +@ConfigurationProperties. See the Refresh Scope documentation for +more information.

    To expose the /actuator/bus-refresh endpoint, you need to add following configuration to your +application:

    management.endpoints.web.exposure.include=bus-refresh

    43.2 Bus Env Endpoint

    The /actuator/bus-env endpoint updates each instances environment with the specified +key/value pair across multiple instances.

    To expose the /actuator/bus-env endpoint, you need to add following configuration to your +application:

    management.endpoints.web.exposure.include=bus-env

    The /actuator/bus-env endpoint accepts POST requests with the following shape:

    {
    +	"name": "key1",
    +	"value": "value1"
    +}

    44. Addressing an Instance

    Each instance of the application has a service ID, whose value can be set with +spring.cloud.bus.id and whose value is expected to be a colon-separated list of +identifiers, in order from least specific to most specific. The default value is +constructed from the environment as a combination of the spring.application.name and +server.port (or spring.application.index, if set). The default value of the ID is +constructed in the form of app:index:id, where:

    • app is the vcap.application.name, if it exists, or spring.application.name
    • index is the vcap.application.instance_index, if it exists, +spring.application.index, local.server.port, server.port, or 0 (in that order).
    • id is the vcap.application.instance_id, if it exists, or a random value.

    The HTTP endpoints accept a destination path parameter, such as +/bus-refresh/customers:9000, where destination is a service ID. If the ID +is owned by an instance on the bus, it processes the message, and all other instances +ignore it.

    45. Addressing All Instances of a Service

    The destination parameter is used in a Spring PathMatcher (with the path separator +as a colon — :) to determine if an instance processes the message. Using the example +from earlier, /bus-env/customers:** targets all instances of the +customers service regardless of the rest of the service ID.

    46. Service ID Must Be Unique

    The bus tries twice to eliminate processing an event — once from the original +ApplicationEvent and once from the queue. To do so, it checks the sending service ID +against the current service ID. If multiple instances of a service have the same ID, +events are not processed. When running on a local machine, each service is on a different +port, and that port is part of the ID. Cloud Foundry supplies an index to differentiate. +To ensure that the ID is unique outside Cloud Foundry, set spring.application.index to +something unique for each instance of a service.

    47. Customizing the Message Broker

    Spring Cloud Bus uses Spring Cloud Stream to +broadcast the messages. So, to get messages to flow, you need only include the binder +implementation of your choice in the classpath. There are convenient starters for the bus +with AMQP (RabbitMQ) and Kafka (spring-cloud-starter-bus-[amqp|kafka]). Generally +speaking, Spring Cloud Stream relies on Spring Boot autoconfiguration conventions for +configuring middleware. For instance, the AMQP broker address can be changed with +spring.rabbitmq.* configuration properties. Spring Cloud Bus has a handful of +native configuration properties in spring.cloud.bus.* (for example, +spring.cloud.bus.destination is the name of the topic to use as the external +middleware). Normally, the defaults suffice.

    To learn more about how to customize the message broker settings, consult the Spring Cloud +Stream documentation.

    48. Tracing Bus Events

    Bus events (subclasses of RemoteApplicationEvent) can be traced by setting +spring.cloud.bus.trace.enabled=true. If you do so, the Spring Boot TraceRepository +(if it is present) shows each event sent and all the acks from each service instance. The +following example comes from the /trace endpoint:

    {
    +  "timestamp": "2015-11-26T10:24:44.411+0000",
    +  "info": {
    +    "signal": "spring.cloud.bus.ack",
    +    "type": "RefreshRemoteApplicationEvent",
    +    "id": "c4d374b7-58ea-4928-a312-31984def293b",
    +    "origin": "stores:8081",
    +    "destination": "*:**"
    +  }
    +  },
    +  {
    +  "timestamp": "2015-11-26T10:24:41.864+0000",
    +  "info": {
    +    "signal": "spring.cloud.bus.sent",
    +    "type": "RefreshRemoteApplicationEvent",
    +    "id": "c4d374b7-58ea-4928-a312-31984def293b",
    +    "origin": "customers:9000",
    +    "destination": "*:**"
    +  }
    +  },
    +  {
    +  "timestamp": "2015-11-26T10:24:41.862+0000",
    +  "info": {
    +    "signal": "spring.cloud.bus.ack",
    +    "type": "RefreshRemoteApplicationEvent",
    +    "id": "c4d374b7-58ea-4928-a312-31984def293b",
    +    "origin": "customers:9000",
    +    "destination": "*:**"
    +  }
    +}

    The preceding trace shows that a RefreshRemoteApplicationEvent was sent from +customers:9000, broadcast to all services, and received (acked) by customers:9000 and +stores:8081.

    To handle the ack signals yourself, you could add an @EventListener for the +AckRemoteApplicationEvent and SentApplicationEvent types to your app (and enable +tracing). Alternatively, you could tap into the TraceRepository and mine the data from +there.

    [Note]Note

    Any Bus application can trace acks. However, sometimes, it is +useful to do this in a central service that can do more complex +queries on the data or forward it to a specialized tracing service.

    49. Broadcasting Your Own Events

    The Bus can carry any event of type RemoteApplicationEvent. The default transport is +JSON, and the deserializer needs to know which types are going to be used ahead of time. +To register a new type, you must put it in a subpackage of +org.springframework.cloud.bus.event.

    To customise the event name, you can use @JsonTypeName on your custom class or rely on +the default strategy, which is to use the simple name of the class.

    [Note]Note

    Both the producer and the consumer need access to the class definition.

    49.1 Registering events in custom packages

    If you cannot or do not want to use a subpackage of org.springframework.cloud.bus.event +for your custom events, you must specify which packages to scan for events of type +RemoteApplicationEvent by using the @RemoteApplicationEventScan annotation. Packages +specified with @RemoteApplicationEventScan include subpackages.

    For example, consider the following custom event, called MyEvent:

    package com.acme;
    +
    +public class MyEvent extends RemoteApplicationEvent {
    +    ...
    +}

    You can register that event with the deserializer in the following way:

    package com.acme;
    +
    +@Configuration
    +@RemoteApplicationEventScan
    +public class BusConfiguration {
    +    ...
    +}

    Without specifying a value, the package of the class where @RemoteApplicationEventScan +is used is registered. In this example, com.acme is registered by using the package of +BusConfiguration.

    You can also explicitly specify the packages to scan by using the value, basePackages +or basePackageClasses properties on @RemoteApplicationEventScan, as shown in the +following example:

    package com.acme;
    +
    +@Configuration
    +//@RemoteApplicationEventScan({"com.acme", "foo.bar"})
    +//@RemoteApplicationEventScan(basePackages = {"com.acme", "foo.bar", "fizz.buzz"})
    +@RemoteApplicationEventScan(basePackageClasses = BusConfiguration.class)
    +public class BusConfiguration {
    +    ...
    +}

    All of the preceding examples of @RemoteApplicationEventScan are equivalent, in that the +com.acme package is registered by explicitly specifying the packages on +@RemoteApplicationEventScan.

    [Note]Note

    You can specify multiple base packages to scan.

    Part VIII. Spring Cloud Sleuth

    Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant

    Greenwich.SR5

    50. Introduction

    Spring Cloud Sleuth implements a distributed tracing solution for Spring Cloud.

    50.1 Terminology

    Spring Cloud Sleuth borrows Dapper’s terminology.

    Span: The basic unit of work. For example, sending an RPC is a new span, as is sending a response to an RPC. +Spans are identified by a unique 64-bit ID for the span and another 64-bit ID for the trace the span is a part of. +Spans also have other data, such as descriptions, timestamped events, key-value annotations (tags), the ID of the span that caused them, and process IDs (normally IP addresses).

    Spans can be started and stopped, and they keep track of their timing information. +Once you create a span, you must stop it at some point in the future.

    [Tip]Tip

    The initial span that starts a trace is called a root span. The value of the ID +of that span is equal to the trace ID.

    Trace: A set of spans forming a tree-like structure. +For example, if you run a distributed big-data store, a trace might be formed by a PUT request.

    Annotation: Used to record the existence of an event in time. With +Brave instrumentation, we no longer need to set special events +for Zipkin to understand who the client and server are, where +the request started, and where it ended. For learning purposes, +however, we mark these events to highlight what kind +of an action took place.

    • cs: Client Sent. The client has made a request. This annotation indicates the start of the span.
    • sr: Server Received: The server side got the request and started processing it. +Subtracting the cs timestamp from this timestamp reveals the network latency.
    • ss: Server Sent. Annotated upon completion of request processing (when the response got sent back to the client). +Subtracting the sr timestamp from this timestamp reveals the time needed by the server side to process the request.
    • cr: Client Received. Signifies the end of the span. +The client has successfully received the response from the server side. +Subtracting the cs timestamp from this timestamp reveals the whole time needed by the client to receive the response from the server.

    The following image shows how Span and Trace look in a system, together with the Zipkin annotations:

    Trace Info propagation

    Each color of a note signifies a span (there are seven spans - from A to G). +Consider the following note:

    Trace Id = X
    +Span Id = D
    +Client Sent

    This note indicates that the current span has Trace Id set to X and Span Id set to D. +Also, the Client Sent event took place.

    The following image shows how parent-child relationships of spans look:

    Parent child relationship

    50.2 Purpose

    The following sections refer to the example shown in the preceding image.

    50.2.1 Distributed Tracing with Zipkin

    This example has seven spans. +If you go to traces in Zipkin, you can see this number in the second trace, as shown in the following image:

    Traces

    However, if you pick a particular trace, you can see four spans, as shown in the following image:

    Traces Info propagation
    [Note]Note

    When you pick a particular trace, you see merged spans. +That means that, if there were two spans sent to Zipkin with Server Received and Server Sent or Client Received and Client Sent annotations, they are presented as a single span.

    Why is there a difference between the seven and four spans in this case?

    • One span comes from the http:/start span. It has the Server Received (sr) and Server Sent (ss) annotations.
    • Two spans come from the RPC call from service1 to service2 to the http:/foo endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service1 side. +Server Received (sr) and Server Sent (ss) events took place on the service2 side. +These two spans form one logical span related to an RPC call.
    • Two spans come from the RPC call from service2 to service3 to the http:/bar endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service2 side. +The Server Received (sr) and Server Sent (ss) events took place on the service3 side. +These two spans form one logical span related to an RPC call.
    • Two spans come from the RPC call from service2 to service4 to the http:/baz endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service2 side. +Server Received (sr) and Server Sent (ss) events took place on the service4 side. +These two spans form one logical span related to an RPC call.

    So, if we count the physical spans, we have one from http:/start, two from service1 calling service2, two from service2 +calling service3, and two from service2 calling service4. In sum, we have a total of seven spans.

    Logically, we see the information of four total Spans because we have one span related to the incoming request +to service1 and three spans related to RPC calls.

    50.2.2 Visualizing errors

    Zipkin lets you visualize errors in your trace. +When an exception was thrown and was not caught, we set proper tags on the span, which Zipkin can then properly colorize. +You could see in the list of traces one trace that is red. That appears because an exception was thrown.

    If you click that trace, you see a similar picture, as follows:

    Error Traces

    If you then click on one of the spans, you see the following

    Error Traces Info propagation

    The span shows the reason for the error and the whole stack trace related to it.

    50.2.3 Distributed Tracing with Brave

    Starting with version 2.0.0, Spring Cloud Sleuth uses Brave as the tracing library. +Consequently, Sleuth no longer takes care of storing the context but delegates that work to Brave.

    Due to the fact that Sleuth had different naming and tagging conventions than Brave, we decided to follow Brave’s conventions from now on. +However, if you want to use the legacy Sleuth approaches, you can set the spring.sleuth.http.legacy.enabled property to true.

    50.2.4 Live examples

    Figure 50.1. Click the Pivotal Web Services icon to see it live!

    Zipkin deployed on Pivotal Web Services

    Click here to see it live!

    The dependency graph in Zipkin should resemble the following image:

    Dependencies

    Figure 50.2. Click the Pivotal Web Services icon to see it live!

    Zipkin deployed on Pivotal Web Services

    Click here to see it live!

    50.2.5 Log correlation

    When using grep to read the logs of those four applications by scanning for a trace ID equal to (for example) 2485ec27856c56f4, you get output resembling the following:

    service1.log:2016-02-26 11:15:47.561  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Hello from service1. Calling service2
    +service2.log:2016-02-26 11:15:47.710  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Hello from service2. Calling service3 and then service4
    +service3.log:2016-02-26 11:15:47.895  INFO [service3,2485ec27856c56f4,1210be13194bfe5,true] 68060 --- [nio-8083-exec-1] i.s.c.sleuth.docs.service3.Application   : Hello from service3
    +service2.log:2016-02-26 11:15:47.924  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service3 [Hello from service3]
    +service4.log:2016-02-26 11:15:48.134  INFO [service4,2485ec27856c56f4,1b1845262ffba49d,true] 68061 --- [nio-8084-exec-1] i.s.c.sleuth.docs.service4.Application   : Hello from service4
    +service2.log:2016-02-26 11:15:48.156  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service4 [Hello from service4]
    +service1.log:2016-02-26 11:15:48.182  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Got response from service2 [Hello from service2, response from service3 [Hello from service3] and from service4 [Hello from service4]]

    If you use a log aggregating tool (such as Kibana, Splunk, and others), you can order the events that took place. +An example from Kibana would resemble the following image:

    Log correlation with Kibana

    If you want to use Logstash, the following listing shows the Grok pattern for Logstash:

    filter {
    +       # pattern matching logback pattern
    +       grok {
    +              match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
    +       }
    +}
    [Note]Note

    If you want to use Grok together with the logs from Cloud Foundry, you have to use the following pattern:

    filter {
    +       # pattern matching logback pattern
    +       grok {
    +              match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
    +       }
    +}

    JSON Logback with Logstash

    Often, you do not want to store your logs in a text file but in a JSON file that Logstash can immediately pick. +To do so, you have to do the following (for readability, we pass the dependencies in the groupId:artifactId:version notation).

    Dependencies Setup

    1. Ensure that Logback is on the classpath (ch.qos.logback:logback-core).
    2. Add Logstash Logback encode. For example, to use version 4.6, add net.logstash.logback:logstash-logback-encoder:4.6.

    Logback Setup

    Consider the following example of a Logback configuration file (named logback-spring.xml).

    <?xml version="1.0" encoding="UTF-8"?>
    +<configuration>
    +	<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    +	​
    +	<springProperty scope="context" name="springAppName" source="spring.application.name"/>
    +	<!-- Example for logging into the build folder of your project -->
    +	<property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>​
    +
    +	<!-- You can override this to have a custom pattern -->
    +	<property name="CONSOLE_LOG_PATTERN"
    +			  value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    +
    +	<!-- Appender to log to console -->
    +	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    +		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    +			<!-- Minimum logging level to be presented in the console logs-->
    +			<level>DEBUG</level>
    +		</filter>
    +		<encoder>
    +			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
    +			<charset>utf8</charset>
    +		</encoder>
    +	</appender>
    +
    +	<!-- Appender to log to file -->​
    +	<appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
    +		<file>${LOG_FILE}</file>
    +		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    +			<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
    +			<maxHistory>7</maxHistory>
    +		</rollingPolicy>
    +		<encoder>
    +			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
    +			<charset>utf8</charset>
    +		</encoder>
    +	</appender>
    +	​
    +	<!-- Appender to log to file in a JSON format -->
    +	<appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
    +		<file>${LOG_FILE}.json</file>
    +		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    +			<fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
    +			<maxHistory>7</maxHistory>
    +		</rollingPolicy>
    +		<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
    +			<providers>
    +				<timestamp>
    +					<timeZone>UTC</timeZone>
    +				</timestamp>
    +				<pattern>
    +					<pattern>
    +						{
    +						"severity": "%level",
    +						"service": "${springAppName:-}",
    +						"trace": "%X{X-B3-TraceId:-}",
    +						"span": "%X{X-B3-SpanId:-}",
    +						"parent": "%X{X-B3-ParentSpanId:-}",
    +						"exportable": "%X{X-Span-Export:-}",
    +						"pid": "${PID:-}",
    +						"thread": "%thread",
    +						"class": "%logger{40}",
    +						"rest": "%message"
    +						}
    +					</pattern>
    +				</pattern>
    +			</providers>
    +		</encoder>
    +	</appender>
    +	​
    +	<root level="INFO">
    +		<appender-ref ref="console"/>
    +		<!-- uncomment this to have also JSON logs -->
    +		<!--<appender-ref ref="logstash"/>-->
    +		<!--<appender-ref ref="flatfile"/>-->
    +	</root>
    +</configuration>

    That Logback configuration file:

    • Logs information from the application in a JSON format to a build/${spring.application.name}.json file.
    • Has commented out two additional appenders: console and standard log file.
    • Has the same logging pattern as the one presented in the previous section.
    [Note]Note

    If you use a custom logback-spring.xml, you must pass the spring.application.name in the bootstrap rather than the application property file. +Otherwise, your custom logback file does not properly read the property.

    50.2.6 Propagating Span Context

    The span context is the state that must get propagated to any child spans across process boundaries. +Part of the Span Context is the Baggage. The trace and span IDs are a required part of the span context. +Baggage is an optional part.

    Baggage is a set of key:value pairs stored in the span context. +Baggage travels together with the trace and is attached to every span. +Spring Cloud Sleuth understands that a header is baggage-related if the HTTP header is prefixed with baggage- and, for messaging, it starts with baggage_.

    [Important]Important

    There is currently no limitation of the count or size of baggage items. +However, keep in mind that too many can decrease system throughput or increase RPC latency. +In extreme cases, too much baggage can crash the application, due to exceeding transport-level message or header capacity.

    The following example shows setting baggage on a span:

    Span initialSpan = this.tracer.nextSpan().name("span").start();
    +ExtraFieldPropagation.set(initialSpan.context(), "foo", "bar");
    +ExtraFieldPropagation.set(initialSpan.context(), "UPPER_CASE", "someValue");

    Baggage versus Span Tags

    Baggage travels with the trace (every child span contains the baggage of its parent). +Zipkin has no knowledge of baggage and does not receive that information.

    [Important]Important

    Starting from Sleuth 2.0.0 you have to pass the baggage key names explicitly +in your project configuration. Read more about that setup here

    Tags are attached to a specific span. In other words, they are presented only for that particular span. +However, you can search by tag to find the trace, assuming a span having the searched tag value exists.

    If you want to be able to lookup a span based on baggage, you should add a corresponding entry as a tag in the root span.

    [Important]Important

    The span must be in scope.

    The following listing shows integration tests that use baggage:

    The setup.  +

    spring.sleuth:
    +  baggage-keys:
    +    - baz
    +    - bizarrecase
    +  propagation-keys:
    +    - foo
    +    - upper_case

    +

    The code.  +

    initialSpan.tag("foo",
    +		ExtraFieldPropagation.get(initialSpan.context(), "foo"));
    +initialSpan.tag("UPPER_CASE",
    +		ExtraFieldPropagation.get(initialSpan.context(), "UPPER_CASE"));

    +

    50.3 Adding Sleuth to the Project

    This section addresses how to add Sleuth to your project with either Maven or Gradle.

    [Important]Important

    To ensure that your application name is properly displayed in Zipkin, set the spring.application.name property in bootstrap.yml.

    50.3.1 Only Sleuth (log correlation)

    If you want to use only Spring Cloud Sleuth without the Zipkin integration, add the spring-cloud-starter-sleuth module to your project.

    The following example shows how to add Sleuth with Maven:

    Maven.  +

    <dependencyManagement> 1
    +      <dependencies>
    +          <dependency>
    +              <groupId>org.springframework.cloud</groupId>
    +              <artifactId>spring-cloud-dependencies</artifactId>
    +              <version>${release.train.version}</version>
    +              <type>pom</type>
    +              <scope>import</scope>
    +          </dependency>
    +      </dependencies>
    +</dependencyManagement>
    +
    +<dependency> 2
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-sleuth</artifactId>
    +</dependency>

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-sleuth.

    The following example shows how to add Sleuth with Gradle:

    Gradle.  +

    dependencyManagement { 1
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    +    }
    +}
    +
    +dependencies { 2
    +    compile "org.springframework.cloud:spring-cloud-starter-sleuth"
    +}

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-sleuth.

    50.3.2 Sleuth with Zipkin via HTTP

    If you want both Sleuth and Zipkin, add the spring-cloud-starter-zipkin dependency.

    The following example shows how to do so for Maven:

    Maven.  +

    <dependencyManagement> 1
    +      <dependencies>
    +          <dependency>
    +              <groupId>org.springframework.cloud</groupId>
    +              <artifactId>spring-cloud-dependencies</artifactId>
    +              <version>${release.train.version}</version>
    +              <type>pom</type>
    +              <scope>import</scope>
    +          </dependency>
    +      </dependencies>
    +</dependencyManagement>
    +
    +<dependency> 2
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-zipkin</artifactId>
    +</dependency>

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin.

    The following example shows how to do so for Gradle:

    Gradle.  +

    dependencyManagement { 1
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    +    }
    +}
    +
    +dependencies { 2
    +    compile "org.springframework.cloud:spring-cloud-starter-zipkin"
    +}

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin.

    50.3.3 Sleuth with Zipkin over RabbitMQ or Kafka

    If you want to use RabbitMQ or Kafka instead of HTTP, add the spring-rabbit or spring-kafka dependency. +The default destination name is zipkin.

    If using Kafka, you must set the property spring.zipkin.sender.type property accordingly:

    spring.zipkin.sender.type: kafka
    [Caution]Caution

    spring-cloud-sleuth-stream is deprecated and incompatible with these destinations.

    If you want Sleuth over RabbitMQ, add the spring-cloud-starter-zipkin and spring-rabbit +dependencies.

    The following example shows how to do so for Gradle:

    Maven.  +

    <dependencyManagement> 1
    +      <dependencies>
    +          <dependency>
    +              <groupId>org.springframework.cloud</groupId>
    +              <artifactId>spring-cloud-dependencies</artifactId>
    +              <version>${release.train.version}</version>
    +              <type>pom</type>
    +              <scope>import</scope>
    +          </dependency>
    +      </dependencies>
    +</dependencyManagement>
    +
    +<dependency> 2
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-zipkin</artifactId>
    +</dependency>
    +<dependency> 3
    +    <groupId>org.springframework.amqp</groupId>
    +    <artifactId>spring-rabbit</artifactId>
    +</dependency>

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin. That way, all nested dependencies get downloaded.

    3

    To automatically configure RabbitMQ, add the spring-rabbit dependency.

    Gradle.  +

    dependencyManagement { 1
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}"
    +    }
    +}
    +
    +dependencies {
    +    compile "org.springframework.cloud:spring-cloud-starter-zipkin" 2
    +    compile "org.springframework.amqp:spring-rabbit" 3
    +}

    +

    1

    We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself.

    2

    Add the dependency to spring-cloud-starter-zipkin. That way, all nested dependencies get downloaded.

    3

    To automatically configure RabbitMQ, add the spring-rabbit dependency.

    50.4 Overriding the auto-configuration of Zipkin

    Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. +In order to get this to work, every tracing system needs to have a Reporter<Span> and Sender. +If you want to override the provided beans you need to give them a specific name. +To do this you can use respectively ZipkinAutoConfiguration.REPORTER_BEAN_NAME and ZipkinAutoConfiguration.SENDER_BEAN_NAME.

    @Configuration
    +protected static class MyConfig {
    +
    +	@Bean(ZipkinAutoConfiguration.REPORTER_BEAN_NAME)
    +	Reporter<zipkin2.Span> myReporter() {
    +		return AsyncReporter.create(mySender());
    +	}
    +
    +	@Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME)
    +	MySender mySender() {
    +		return new MySender();
    +	}
    +
    +	static class MySender extends Sender {
    +
    +		private boolean spanSent = false;
    +
    +		boolean isSpanSent() {
    +			return this.spanSent;
    +		}
    +
    +		@Override
    +		public Encoding encoding() {
    +			return Encoding.JSON;
    +		}
    +
    +		@Override
    +		public int messageMaxBytes() {
    +			return Integer.MAX_VALUE;
    +		}
    +
    +		@Override
    +		public int messageSizeInBytes(List<byte[]> encodedSpans) {
    +			return encoding().listSizeInBytes(encodedSpans);
    +		}
    +
    +		@Override
    +		public Call<Void> sendSpans(List<byte[]> encodedSpans) {
    +			this.spanSent = true;
    +			return Call.create(null);
    +		}
    +
    +	}
    +
    +}

    51. Additional Resources

    You can watch a video of Reshmi Krishna and Marcin Grzejszczak talking about Spring Cloud +Sleuth and Zipkin by clicking here.

    You can check different setups of Sleuth and Brave in the openzipkin/sleuth-webmvc-example repository.

    52. Features

    • Adds trace and span IDs to the Slf4J MDC, so you can extract all the logs from a given trace or span in a log aggregator, as shown in the following example logs:

      2016-02-02 15:30:57.902  INFO [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
      +2016-02-02 15:30:58.372 ERROR [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ...
      +2016-02-02 15:31:01.936  INFO [bar,46ab0d418373cbc9,46ab0d418373cbc9,false] 23030 --- [nio-8081-exec-4] ...

      Notice the [appname,traceId,spanId,exportable] entries from the MDC:

      • spanId: The ID of a specific operation that took place.
      • appname: The name of the application that logged the span.
      • traceId: The ID of the latency graph that contains the span.
      • exportable: Whether the log should be exported to Zipkin. +When would you like the span not to be exportable? +When you want to wrap some operation in a Span and have it written to the logs only.
    • Provides an abstraction over common distributed tracing data models: traces, spans (forming a DAG), annotations, and key-value annotations. +Spring Cloud Sleuth is loosely based on HTrace but is compatible with Zipkin (Dapper).
    • Sleuth records timing information to aid in latency analysis. +By using sleuth, you can pinpoint causes of latency in your applications.
    • Sleuth is written to not log too much and to not cause your production application to crash. +To that end, Sleuth:

      • Propagates structural data about your call graph in-band and the rest out-of-band.
      • Includes opinionated instrumentation of layers such as HTTP.
      • Includes a sampling policy to manage volume.
      • Can report to a Zipkin system for query and visualization.
    • Instruments common ingress and egress points from Spring applications (servlet filter, async endpoints, rest template, scheduled actions, message channels, Zuul filters, and Feign client).
    • Sleuth includes default logic to join a trace across HTTP or messaging boundaries. +For example, HTTP propagation works over Zipkin-compatible request headers.
    • Sleuth can propagate context (also known as baggage) between processes. +Consequently, if you set a baggage element on a Span, it is sent downstream to other processes over either HTTP or messaging.
    • Provides a way to create or continue spans and add tags and logs through annotations.
    • If spring-cloud-sleuth-zipkin is on the classpath, the app generates and collects Zipkin-compatible traces. +By default, it sends them over HTTP to a Zipkin server on localhost (port 9411). +You can configure the location of the service by setting spring.zipkin.baseUrl.

      • If you depend on spring-rabbit, your app sends traces to a RabbitMQ broker instead of HTTP.
      • If you depend on spring-kafka, and set spring.zipkin.sender.type: kafka, your app sends traces to a Kafka broker instead of HTTP.
    [Caution]Caution

    spring-cloud-sleuth-stream is deprecated and should no longer be used.

    [Important]Important

    If you use Zipkin, configure the probability of spans exported by setting spring.sleuth.sampler.probability +(default: 0.1, which is 10 percent). Otherwise, you might think that Sleuth is not working be cause it omits some spans.

    [Note]Note

    The SLF4J MDC is always set and logback users immediately see the trace and span IDs in logs per the example +shown earlier. +Other logging systems have to configure their own formatter to get the same result. +The default is as follows: +logging.pattern.level set to %5p [${spring.zipkin.service.name:${spring.application.name:-}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}] +(this is a Spring Boot feature for logback users). +If you do not use SLF4J, this pattern is NOT automatically applied.

    52.1 Introduction to Brave

    [Important]Important

    Starting with version 2.0.0, Spring Cloud Sleuth uses +Brave as the tracing library. +For your convenience, we embed part of the Brave’s docs here.

    [Important]Important

    In the vast majority of cases you need to just use the Tracer +or SpanCustomizer beans from Brave that Sleuth provides. The documentation below contains +a high overview of what Brave is and how it works.

    Brave is a library used to capture and report latency information about distributed operations to Zipkin. +Most users do not use Brave directly. They use libraries or frameworks rather than employ Brave on their behalf.

    This module includes a tracer that creates and joins spans that model the latency of potentially distributed work. +It also includes libraries to propagate the trace context over network boundaries (for example, with HTTP headers).

    52.1.1 Tracing

    Most importantly, you need a brave.Tracer, configured to report to Zipkin.

    The following example setup sends trace data (spans) to Zipkin over HTTP (as opposed to Kafka):

    class MyClass {
    +
    +    private final Tracer tracer;
    +
    +    // Tracer will be autowired
    +    MyClass(Tracer tracer) {
    +        this.tracer = tracer;
    +    }
    +
    +    void doSth() {
    +        Span span = tracer.newTrace().name("encode").start();
    +        // ...
    +    }
    +}
    [Important]Important

    If your span contains a name longer than 50 chars, then that name is truncated to 50 chars. +Your names have to be explicit and concrete. +Big names lead to latency issues and sometimes even thrown exceptions.

    The tracer creates and joins spans that model the latency of potentially distributed work. +It can employ sampling to reduce overhead during the process, to reduce the amount of data sent to Zipkin, or both.

    Spans returned by a tracer report data to Zipkin when finished or do nothing if unsampled. +After starting a span, you can annotate events of interest or add tags containing details or lookup keys.

    Spans have a context that includes trace identifiers that place the span at the correct spot in the tree representing the distributed operation.

    52.1.2 Local Tracing

    When tracing code that never leaves your process, run it inside a scoped span.

    @Autowired Tracer tracer;
    +
    +// Start a new trace or a span within an existing trace representing an operation
    +ScopedSpan span = tracer.startScopedSpan("encode");
    +try {
    +  // The span is in "scope" meaning downstream code such as loggers can see trace IDs
    +  return encoder.encode();
    +} catch (RuntimeException | Error e) {
    +  span.error(e); // Unless you handle exceptions, you might not know the operation failed!
    +  throw e;
    +} finally {
    +  span.finish(); // always finish the span
    +}

    When you need more features, or finer control, use the Span type:

    @Autowired Tracer tracer;
    +
    +// Start a new trace or a span within an existing trace representing an operation
    +Span span = tracer.nextSpan().name("encode").start();
    +// Put the span in "scope" so that downstream code such as loggers can see trace IDs
    +try (SpanInScope ws = tracer.withSpanInScope(span)) {
    +  return encoder.encode();
    +} catch (RuntimeException | Error e) {
    +  span.error(e); // Unless you handle exceptions, you might not know the operation failed!
    +  throw e;
    +} finally {
    +  span.finish(); // note the scope is independent of the span. Always finish a span.
    +}

    Both of the above examples report the exact same span on finish!

    In the above example, the span will be either a new root span or the +next child in an existing trace.

    52.1.3 Customizing Spans

    Once you have a span, you can add tags to it. +The tags can be used as lookup keys or details. +For example, you might add a tag with your runtime version, as shown in the following example:

    span.tag("clnt/finagle.version", "6.36.0");

    When exposing the ability to customize spans to third parties, prefer brave.SpanCustomizer as opposed to brave.Span. +The former is simpler to understand and test and does not tempt users with span lifecycle hooks.

    interface MyTraceCallback {
    +  void request(Request request, SpanCustomizer customizer);
    +}

    Since brave.Span implements brave.SpanCustomizer, you can pass it to users, as shown in the following example:

    for (MyTraceCallback callback : userCallbacks) {
    +  callback.request(request, span);
    +}

    52.1.4 Implicitly Looking up the Current Span

    Sometimes, you do not know if a trace is in progress or not, and you do not want users to do null checks. +brave.CurrentSpanCustomizer handles this problem by adding data to any span that’s in progress or drops, as shown in the following example:

    Ex.

    // The user code can then inject this without a chance of it being null.
    +@Autowired SpanCustomizer span;
    +
    +void userCode() {
    +  span.annotate("tx.started");
    +  ...
    +}

    52.1.5 RPC tracing

    [Tip]Tip

    Check for instrumentation written here and Zipkin’s list before rolling your own RPC instrumentation.

    RPC tracing is often done automatically by interceptors. Behind the scenes, they add tags and events that relate to their role in an RPC operation.

    The following example shows how to add a client span:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// before you send a request, add metadata that describes the operation
    +span = tracer.nextSpan().name(service + "/" + method).kind(CLIENT);
    +span.tag("myrpc.version", "1.0.0");
    +span.remoteServiceName("backend");
    +span.remoteIpAndPort("172.3.4.1", 8108);
    +
    +// Add the trace context to the request, so it can be propagated in-band
    +tracing.propagation().injector(Request::addHeader)
    +                     .inject(span.context(), request);
    +
    +// when the request is scheduled, start the span
    +span.start();
    +
    +// if there is an error, tag the span
    +span.tag("error", error.getCode());
    +// or if there is an exception
    +span.error(exception);
    +
    +// when the response is complete, finish the span
    +span.finish();

    One-Way tracing

    Sometimes, you need to model an asynchronous operation where there is a +request but no response. In normal RPC tracing, you use span.finish() +to indicate that the response was received. In one-way tracing, you use +span.flush() instead, as you do not expect a response.

    The following example shows how a client might model a one-way operation:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// start a new span representing a client request
    +oneWaySend = tracer.nextSpan().name(service + "/" + method).kind(CLIENT);
    +
    +// Add the trace context to the request, so it can be propagated in-band
    +tracing.propagation().injector(Request::addHeader)
    +                     .inject(oneWaySend.context(), request);
    +
    +// fire off the request asynchronously, totally dropping any response
    +request.execute();
    +
    +// start the client side and flush instead of finish
    +oneWaySend.start().flush();

    The following example shows how a server might handle a one-way operation:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// pull the context out of the incoming request
    +extractor = tracing.propagation().extractor(Request::getHeader);
    +
    +// convert that context to a span which you can name and add tags to
    +oneWayReceive = nextSpan(tracer, extractor.extract(request))
    +    .name("process-request")
    +    .kind(SERVER)
    +    ... add tags etc.
    +
    +// start the server side and flush instead of finish
    +oneWayReceive.start().flush();
    +
    +// you should not modify this span anymore as it is complete. However,
    +// you can create children to represent follow-up work.
    +next = tracer.newSpan(oneWayReceive.context()).name("step2").start();

    53. Sampling

    Sampling may be employed to reduce the data collected and reported out of process. +When a span is not sampled, it adds no overhead (a noop).

    Sampling is an up-front decision, meaning that the decision to report data is made at the first operation in a trace and that decision is propagated downstream.

    By default, a global sampler applies a single rate to all traced operations. +Tracer.Builder.sampler controls this setting, and it defaults to tracing every request.

    53.1 Declarative sampling

    Some applications need to sample based on the type or annotations of a java method.

    Most users use a framework interceptor to automate this sort of policy. +The following example shows how that might work internally:

    @Autowired Tracer tracer;
    +
    +// derives a sample rate from an annotation on a java method
    +DeclarativeSampler<Traced> sampler = DeclarativeSampler.create(Traced::sampleRate);
    +
    +@Around("@annotation(traced)")
    +public Object traceThing(ProceedingJoinPoint pjp, Traced traced) throws Throwable {
    +  // When there is no trace in progress, this decides using an annotation
    +  Sampler decideUsingAnnotation = declarativeSampler.toSampler(traced);
    +  Tracer tracer = tracer.withSampler(decideUsingAnnotation);
    +
    +  // This code looks the same as if there was no declarative override
    +  ScopedSpan span = tracer.startScopedSpan(spanName(pjp));
    +  try {
    +    return pjp.proceed();
    +  } catch (RuntimeException | Error e) {
    +    span.error(e);
    +    throw e;
    +  } finally {
    +    span.finish();
    +  }
    +}

    53.2 Custom sampling

    Depending on what the operation is, you may want to apply different policies. +For example, you might not want to trace requests to static resources such as images, or you might want to trace all requests to a new api.

    Most users use a framework interceptor to automate this sort of policy. +The following example shows how that might work internally:

    @Autowired Tracer tracer;
    +@Autowired Sampler fallback;
    +
    +Span nextSpan(final Request input) {
    +  Sampler requestBased = Sampler() {
    +    @Override public boolean isSampled(long traceId) {
    +      if (input.url().startsWith("/experimental")) {
    +        return true;
    +      } else if (input.url().startsWith("/static")) {
    +        return false;
    +      }
    +      return fallback.isSampled(traceId);
    +    }
    +  };
    +  return tracer.withSampler(requestBased).nextSpan();
    +}

    53.3 Sampling in Spring Cloud Sleuth

    By default Spring Cloud Sleuth sets all spans to non-exportable. +That means that traces appear in logs but not in any remote store. +For testing the default is often enough, and it probably is all you need if you use only the logs (for example, with an ELK aggregator). +If you export span data to Zipkin, there is also an Sampler.ALWAYS_SAMPLE setting that exports everything and a ProbabilityBasedSampler setting that samples a fixed fraction of spans.

    [Note]Note

    The ProbabilityBasedSampler is the default if you use spring-cloud-sleuth-zipkin. +You can configure the exports by setting spring.sleuth.sampler.probability. +The passed value needs to be a double from 0.0 to 1.0.

    A sampler can be installed by creating a bean definition, as shown in the following example:

    @Bean
    +public Sampler defaultSampler() {
    +	return Sampler.ALWAYS_SAMPLE;
    +}
    [Tip]Tip

    You can set the HTTP header X-B3-Flags to 1, or, when doing messaging, you can set the spanFlags header to 1. +Doing so forces the current span to be exportable regardless of the sampling decision.

    In order to use the rate-limited sampler set the spring.sleuth.sampler.rate property to choose an amount of traces to accept on a per-second interval. The minimum number is 0 and the max is 2,147,483,647 (max int).

    54. Propagation

    Propagation is needed to ensure activities originating from the same root are collected together in the same trace. +The most common propagation approach is to copy a trace context from a client by sending an RPC request to a server receiving it.

    For example, when a downstream HTTP call is made, its trace context is encoded as request headers and sent along with it, as shown in the following image:

       Client Span                                                Server Span
    +┌──────────────────┐                                       ┌──────────────────┐
    +│                  │                                       │                  │
    +│   TraceContext   │           Http Request Headers        │   TraceContext   │
    +│ ┌──────────────┐ │          ┌───────────────────┐        │ ┌──────────────┐ │
    +│ │ TraceId      │ │          │ X─B3─TraceId      │        │ │ TraceId      │ │
    +│ │              │ │          │                   │        │ │              │ │
    +│ │ ParentSpanId │ │ Extract  │ X─B3─ParentSpanId │ Inject │ │ ParentSpanId │ │
    +│ │              ├─┼─────────>│                   ├────────┼>│              │ │
    +│ │ SpanId       │ │          │ X─B3─SpanId       │        │ │ SpanId       │ │
    +│ │              │ │          │                   │        │ │              │ │
    +│ │ Sampled      │ │          │ X─B3─Sampled      │        │ │ Sampled      │ │
    +│ └──────────────┘ │          └───────────────────┘        │ └──────────────┘ │
    +│                  │                                       │                  │
    +└──────────────────┘                                       └──────────────────┘

    The names above are from B3 Propagation, which is built-in to Brave and has implementations in many languages and frameworks.

    Most users use a framework interceptor to automate propagation. +The next two examples show how that might work for a client and a server.

    The following example shows how client-side propagation might work:

    @Autowired Tracing tracing;
    +
    +// configure a function that injects a trace context into a request
    +injector = tracing.propagation().injector(Request.Builder::addHeader);
    +
    +// before a request is sent, add the current span's context to it
    +injector.inject(span.context(), request);

    The following example shows how server-side propagation might work:

    @Autowired Tracing tracing;
    +@Autowired Tracer tracer;
    +
    +// configure a function that extracts the trace context from a request
    +extractor = tracing.propagation().extractor(Request::getHeader);
    +
    +// when a server receives a request, it joins or starts a new trace
    +span = tracer.nextSpan(extractor.extract(request));

    54.1 Propagating extra fields

    Sometimes you need to propagate extra fields, such as a request ID or an alternate trace context. +For example, if you are in a Cloud Foundry environment, you might want to pass the request ID, as shown in the following example:

    // when you initialize the builder, define the extra field you want to propagate
    +Tracing.newBuilder().propagationFactory(
    +  ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-vcap-request-id")
    +);
    +
    +// later, you can tag that request ID or use it in log correlation
    +requestId = ExtraFieldPropagation.get("x-vcap-request-id");

    You may also need to propagate a trace context that you are not using. +For example, you may be in an Amazon Web Services environment but not be reporting data to X-Ray. +To ensure X-Ray can co-exist correctly, pass-through its tracing header, as shown in the following example:

    tracingBuilder.propagationFactory(
    +  ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-amzn-trace-id")
    +);
    [Tip]Tip

    In Spring Cloud Sleuth all elements of the tracing builder Tracing.newBuilder() +are defined as beans. So if you want to pass a custom PropagationFactory, it’s enough +for you to create a bean of that type and we will set it in the Tracing bean.

    54.1.1 Prefixed fields

    If they follow a common pattern, you can also prefix fields. +The following example shows how to propagate x-vcap-request-id the field as-is but send the country-code and user-id fields on the wire as x-baggage-country-code and x-baggage-user-id, respectively:

    Tracing.newBuilder().propagationFactory(
    +  ExtraFieldPropagation.newFactoryBuilder(B3Propagation.FACTORY)
    +                       .addField("x-vcap-request-id")
    +                       .addPrefixedFields("x-baggage-", Arrays.asList("country-code", "user-id"))
    +                       .build()
    +);

    Later, you can call the following code to affect the country code of the current trace context:

    ExtraFieldPropagation.set("x-country-code", "FO");
    +String countryCode = ExtraFieldPropagation.get("x-country-code");

    Alternatively, if you have a reference to a trace context, you can use it explicitly, as shown in the following example:

    ExtraFieldPropagation.set(span.context(), "x-country-code", "FO");
    +String countryCode = ExtraFieldPropagation.get(span.context(), "x-country-code");
    [Important]Important

    A difference from previous versions of Sleuth is that, with Brave, you must pass the list of baggage keys. +There are two properties to achieve this. +With the spring.sleuth.baggage-keys, you set keys that get prefixed with baggage- for HTTP calls and baggage_ for messaging. +You can also use the spring.sleuth.propagation-keys property to pass a list of prefixed keys that are whitelisted without any prefix. +Notice that there’s no x- in front of the header keys.

    In order to automatically set the baggage values to Slf4j’s MDC, you have to set +the spring.sleuth.log.slf4j.whitelisted-mdc-keys property with a list of whitelisted +baggage and propagation keys. E.g. spring.sleuth.log.slf4j.whitelisted-mdc-keys=foo will set the value of the foo baggage into MDC.

    [Important]Important

    Remember that adding entries to MDC can drastically decrease the performance of your application!

    If you want to add the baggage entries as tags, to make it possible to search for spans via the baggage entries, you can set the value of +spring.sleuth.propagation.tag.whitelisted-keys with a list of whitelisted baggage keys. To disable the feature you have to pass the spring.sleuth.propagation.tag.enabled=false property.

    54.1.2 Extracting a Propagated Context

    The TraceContext.Extractor<C> reads trace identifiers and sampling status from an incoming request or message. +The carrier is usually a request object or headers.

    This utility is used in standard instrumentation (such as HttpServerHandler) but can also be used for custom RPC or messaging code.

    TraceContextOrSamplingFlags is usually used only with Tracer.nextSpan(extracted), unless you are +sharing span IDs between a client and a server.

    54.1.3 Sharing span IDs between Client and Server

    A normal instrumentation pattern is to create a span representing the server side of an RPC. +Extractor.extract might return a complete trace context when applied to an incoming client request. +Tracer.joinSpan attempts to continue this trace, using the same span ID if supported or creating a child span +if not. When the span ID is shared, the reported data includes a flag saying so.

    The following image shows an example of B3 propagation:

                                  ┌───────────────────┐      ┌───────────────────┐
    + Incoming Headers             │   TraceContext    │      │   TraceContext    │
    +┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │
    +│ X─B3-TraceId      │─────────┼─┼> TraceId      │ │──────┼─┼> TraceId      │ │
    +│                   │         │ │               │ │      │ │               │ │
    +│ X─B3-ParentSpanId │─────────┼─┼> ParentSpanId │ │──────┼─┼> ParentSpanId │ │
    +│                   │         │ │               │ │      │ │               │ │
    +│ X─B3-SpanId       │─────────┼─┼> SpanId       │ │──────┼─┼> SpanId       │ │
    +└───────────────────┘         │ │               │ │      │ │               │ │
    +                              │ │               │ │      │ │  Shared: true │ │
    +                              │ └───────────────┘ │      │ └───────────────┘ │
    +                              └───────────────────┘      └───────────────────┘

    Some propagation systems forward only the parent span ID, detected when Propagation.Factory.supportsJoin() == false. +In this case, a new span ID is always provisioned, and the incoming context determines the parent ID.

    The following image shows an example of AWS propagation:

                                  ┌───────────────────┐      ┌───────────────────┐
    + x-amzn-trace-id              │   TraceContext    │      │   TraceContext    │
    +┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │
    +│ Root              │─────────┼─┼> TraceId      │ │──────┼─┼> TraceId      │ │
    +│                   │         │ │               │ │      │ │               │ │
    +│ Parent            │─────────┼─┼> SpanId       │ │──────┼─┼> ParentSpanId │ │
    +└───────────────────┘         │ └───────────────┘ │      │ │               │ │
    +                              └───────────────────┘      │ │  SpanId: New  │ │
    +                                                         │ └───────────────┘ │
    +                                                         └───────────────────┘

    Note: Some span reporters do not support sharing span IDs. +For example, if you set Tracing.Builder.spanReporter(amazonXrayOrGoogleStackdrive), you should disable join by setting Tracing.Builder.supportsJoin(false). +Doing so forces a new child span on Tracer.joinSpan().

    54.1.4 Implementing Propagation

    TraceContext.Extractor<C> is implemented by a Propagation.Factory plugin. +Internally, this code creates the union type, TraceContextOrSamplingFlags, with one of the following: +* TraceContext if trace and span IDs were present. +* TraceIdContext if a trace ID was present but span IDs were not present. +* SamplingFlags if no identifiers were present.

    Some Propagation implementations carry extra data from the point of extraction (for example, reading incoming headers) to injection (for example, writing outgoing headers). +For example, it might carry a request ID. +When implementations have extra data, they handle it as follows: +* If a TraceContext were extracted, add the extra data as TraceContext.extra(). +* Otherwise, add it as TraceContextOrSamplingFlags.extra(), which Tracer.nextSpan handles.

    55. Current Tracing Component

    Brave supports a current tracing component concept, which should only be used when you have no other way to get a reference. +This was made for JDBC connections, as they often initialize prior to the tracing component.

    The most recent tracing component instantiated is available through Tracing.current(). +You can also use Tracing.currentTracer() to get only the tracer. +If you use either of these methods, do not cache the result. +Instead, look them up each time you need them.

    56. Current Span

    Brave supports a current span concept which represents the in-flight operation. +You can use Tracer.currentSpan() to add custom tags to a span and Tracer.nextSpan() to create a child of whatever is in-flight.

    [Important]Important

    In Sleuth, you can autowire the Tracer bean to retrieve the current span via +tracer.currentSpan() method. To retrieve the current context just call +tracer.currentSpan().context(). To get the current trace id as String +you can use the traceIdString() method like this: tracer.currentSpan().context().traceIdString().

    56.1 Setting a span in scope manually

    When writing new instrumentation, it is important to place a span you created in scope as the current span. +Not only does doing so let users access it with Tracer.currentSpan(), but it also allows customizations such as SLF4J MDC to see the current trace IDs.

    Tracer.withSpanInScope(Span) facilitates this and is most conveniently employed by using the try-with-resources idiom. +Whenever external code might be invoked (such as proceeding an interceptor or otherwise), place the span in scope, as shown in the following example:

    @Autowired Tracer tracer;
    +
    +try (SpanInScope ws = tracer.withSpanInScope(span)) {
    +  return inboundRequest.invoke();
    +} finally { // note the scope is independent of the span
    +  span.finish();
    +}

    In edge cases, you may need to clear the current span temporarily (for example, launching a task that should not be associated with the current request). To do tso, pass null to withSpanInScope, as shown in the following example:

    @Autowired Tracer tracer;
    +
    +try (SpanInScope cleared = tracer.withSpanInScope(null)) {
    +  startBackgroundThread();
    +}

    57. Instrumentation

    Spring Cloud Sleuth automatically instruments all your Spring applications, so you should not have to do anything to activate it. +The instrumentation is added by using a variety of technologies according to the stack that is available. For example, for a servlet web application, we use a Filter, and, for Spring Integration, we use ChannelInterceptors.

    You can customize the keys used in span tags. +To limit the volume of span data, an HTTP request is, by default, tagged only with a handful of metadata, such as the status code, the host, and the URL. +You can add request headers by configuring spring.sleuth.keys.http.headers (a list of header names).

    [Note]Note

    Tags are collected and exported only if there is a Sampler that allows it. By default, there is no such Sampler, to ensure that there is no danger of accidentally collecting too much data without configuring something).

    58. Span lifecycle

    You can do the following operations on the Span by means of brave.Tracer:

    • start: When you start a span, its name is assigned and the start timestamp is recorded.
    • close: The span gets finished (the end time of the span is recorded) and, if the span is sampled, it is eligible for collection (for example, to Zipkin).
    • continue: A new instance of span is created. +It is a copy of the one that it continues.
    • detach: The span does not get stopped or closed. +It only gets removed from the current thread.
    • create with explicit parent: You can create a new span and set an explicit parent for it.
    [Tip]Tip

    Spring Cloud Sleuth creates an instance of Tracer for you. In order to use it, you can autowire it.

    58.1 Creating and finishing spans

    You can manually create spans by using the Tracer, as shown in the following example:

    // Start a span. If there was a span present in this thread it will become
    +// the `newSpan`'s parent.
    +Span newSpan = this.tracer.nextSpan().name("calculateTax");
    +try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(newSpan.start())) {
    +	// ...
    +	// You can tag a span
    +	newSpan.tag("taxValue", taxValue);
    +	// ...
    +	// You can log an event on a span
    +	newSpan.annotate("taxCalculated");
    +}
    +finally {
    +	// Once done remember to finish the span. This will allow collecting
    +	// the span to send it to Zipkin
    +	newSpan.finish();
    +}

    In the preceding example, we could see how to create a new instance of the span. +If there is already a span in this thread, it becomes the parent of the new span.

    [Important]Important

    Always clean after you create a span. Also, always finish any span that you want to send to Zipkin.

    [Important]Important

    If your span contains a name greater than 50 chars, that name is truncated to 50 chars. +Your names have to be explicit and concrete. Big names lead to latency issues and sometimes even exceptions.

    58.2 Continuing Spans

    Sometimes, you do not want to create a new span but you want to continue one. An example of such a +situation might be as follows:

    • AOP: If there was already a span created before an aspect was reached, you might not want to create a new span.
    • Hystrix: Executing a Hystrix command is most likely a logical part of the current processing. +It is in fact merely a technical implementation detail that you would not necessarily want to reflect in tracing as a separate being.

    To continue a span, you can use brave.Tracer, as shown in the following example:

    // let's assume that we're in a thread Y and we've received
    +// the `initialSpan` from thread X
    +Span continuedSpan = this.tracer.toSpan(newSpan.context());
    +try {
    +	// ...
    +	// You can tag a span
    +	continuedSpan.tag("taxValue", taxValue);
    +	// ...
    +	// You can log an event on a span
    +	continuedSpan.annotate("taxCalculated");
    +}
    +finally {
    +	// Once done remember to flush the span. That means that
    +	// it will get reported but the span itself is not yet finished
    +	continuedSpan.flush();
    +}

    58.3 Creating a Span with an explicit Parent

    You might want to start a new span and provide an explicit parent of that span. +Assume that the parent of a span is in one thread and you want to start a new span in another thread. +In Brave, whenever you call nextSpan(), it creates a span in reference to the span that is currently in scope. +You can put the span in scope and then call nextSpan(), as shown in the following example:

    // let's assume that we're in a thread Y and we've received
    +// the `initialSpan` from thread X. `initialSpan` will be the parent
    +// of the `newSpan`
    +Span newSpan = null;
    +try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(initialSpan)) {
    +	newSpan = this.tracer.nextSpan().name("calculateCommission");
    +	// ...
    +	// You can tag a span
    +	newSpan.tag("commissionValue", commissionValue);
    +	// ...
    +	// You can log an event on a span
    +	newSpan.annotate("commissionCalculated");
    +}
    +finally {
    +	// Once done remember to finish the span. This will allow collecting
    +	// the span to send it to Zipkin. The tags and events set on the
    +	// newSpan will not be present on the parent
    +	if (newSpan != null) {
    +		newSpan.finish();
    +	}
    +}
    [Important]Important

    After creating such a span, you must finish it. Otherwise it is not reported (for example, to Zipkin).

    59. Naming spans

    Picking a span name is not a trivial task. A span name should depict an operation name. +The name should be low cardinality, so it should not include identifiers.

    Since there is a lot of instrumentation going on, some span names are artificial:

    • controller-method-name when received by a Controller with a method name of controllerMethodName
    • async for asynchronous operations done with wrapped Callable and Runnable interfaces.
    • Methods annotated with @Scheduled return the simple name of the class.

    Fortunately, for asynchronous processing, you can provide explicit naming.

    59.1 @SpanName Annotation

    You can name the span explicitly by using the @SpanName annotation, as shown in the following example:

    	@SpanName("calculateTax")
    +	class TaxCountingRunnable implements Runnable {
    +
    +		@Override
    +		public void run() {
    +			// perform logic
    +		}
    +
    +	}
    +
    +}

    In this case, when processed in the following manner, the span is named calculateTax:

    Runnable runnable = new TraceRunnable(this.tracing, spanNamer,
    +		new TaxCountingRunnable());
    +Future<?> future = executorService.submit(runnable);
    +// ... some additional logic ...
    +future.get();

    59.2 toString() method

    It is pretty rare to create separate classes for Runnable or Callable. +Typically, one creates an anonymous instance of those classes. +You cannot annotate such classes. +To overcome that limitation, if there is no @SpanName annotation present, we check whether the class has a custom implementation of the toString() method.

    Running such code leads to creating a span named calculateTax, as shown in the following example:

    Runnable runnable = new TraceRunnable(this.tracing, spanNamer, new Runnable() {
    +	@Override
    +	public void run() {
    +		// perform logic
    +	}
    +
    +	@Override
    +	public String toString() {
    +		return "calculateTax";
    +	}
    +});
    +Future<?> future = executorService.submit(runnable);
    +// ... some additional logic ...
    +future.get();

    60. Managing Spans with Annotations

    You can manage spans with a variety of annotations.

    60.1 Rationale

    There are a number of good reasons to manage spans with annotations, including:

    • API-agnostic means to collaborate with a span. Use of annotations lets users add to a span with no library dependency on a span api. +Doing so lets Sleuth change its core API to create less impact to user code.
    • Reduced surface area for basic span operations. Without this feature, you must use the span api, which has lifecycle commands that could be used incorrectly. +By only exposing scope, tag, and log functionality, you can collaborate without accidentally breaking span lifecycle.
    • Collaboration with runtime generated code. With libraries such as Spring Data and Feign, the implementations of interfaces are generated at runtime. +Consequently, span wrapping of objects was tedious. +Now you can provide annotations over interfaces and the arguments of those interfaces.

    60.2 Creating New Spans

    If you do not want to create local spans manually, you can use the @NewSpan annotation. +Also, we provide the @SpanTag annotation to add tags in an automated fashion.

    Now we can consider some examples of usage.

    @NewSpan
    +void testMethod();

    Annotating the method without any parameter leads to creating a new span whose name equals the annotated method name.

    @NewSpan("customNameOnTestMethod4")
    +void testMethod4();

    If you provide the value in the annotation (either directly or by setting the name parameter), the created span has the provided value as the name.

    // method declaration
    +@NewSpan(name = "customNameOnTestMethod5")
    +void testMethod5(@SpanTag("testTag") String param);
    +
    +// and method execution
    +this.testBean.testMethod5("test");

    You can combine both the name and a tag. Let’s focus on the latter. +In this case, the value of the annotated method’s parameter runtime value becomes the value of the tag. +In our sample, the tag key is testTag, and the tag value is test.

    @NewSpan(name = "customNameOnTestMethod3")
    +@Override
    +public void testMethod3() {
    +}

    You can place the @NewSpan annotation on both the class and an interface. +If you override the interface’s method and provide a different value for the @NewSpan annotation, the most +concrete one wins (in this case customNameOnTestMethod3 is set).

    60.3 Continuing Spans

    If you want to add tags and annotations to an existing span, you can use the @ContinueSpan annotation, as shown in the following example:

    // method declaration
    +@ContinueSpan(log = "testMethod11")
    +void testMethod11(@SpanTag("testTag11") String param);
    +
    +// method execution
    +this.testBean.testMethod11("test");
    +this.testBean.testMethod13();

    (Note that, in contrast with the @NewSpan annotation ,you can also add logs with the log parameter.)

    That way, the span gets continued and:

    • Log entries named testMethod11.before and testMethod11.after are created.
    • If an exception is thrown, a log entry named testMethod11.afterFailure is also created.
    • A tag with a key of testTag11 and a value of test is created.

    60.4 Advanced Tag Setting

    There are 3 different ways to add tags to a span. All of them are controlled by the SpanTag annotation. +The precedence is as follows:

    1. Try with a bean of TagValueResolver type and a provided name.
    2. If the bean name has not been provided, try to evaluate an expression. +We search for a TagValueExpressionResolver bean. +The default implementation uses SPEL expression resolution. +IMPORTANT You can only reference properties from the SPEL expression. Method execution is not allowed due to security constraints.
    3. If we do not find any expression to evaluate, return the toString() value of the parameter.

    60.4.1 Custom extractor

    The value of the tag for the following method is computed by an implementation of TagValueResolver interface. +Its class name has to be passed as the value of the resolver attribute.

    Consider the following annotated method:

    @NewSpan
    +public void getAnnotationForTagValueResolver(
    +		@SpanTag(key = "test", resolver = TagValueResolver.class) String test) {
    +}

    Now further consider the following TagValueResolver bean implementation:

    @Bean(name = "myCustomTagValueResolver")
    +public TagValueResolver tagValueResolver() {
    +	return parameter -> "Value from myCustomTagValueResolver";
    +}

    The two preceding examples lead to setting a tag value equal to Value from myCustomTagValueResolver.

    60.4.2 Resolving Expressions for a Value

    Consider the following annotated method:

    @NewSpan
    +public void getAnnotationForTagValueExpression(
    +		@SpanTag(key = "test", expression = "'hello' + ' characters'") String test) {
    +}

    No custom implementation of a TagValueExpressionResolver leads to evaluation of the SPEL expression, and a tag with a value of 4 characters is set on the span. +If you want to use some other expression resolution mechanism, you can create your own implementation of the bean.

    60.4.3 Using the toString() method

    Consider the following annotated method:

    @NewSpan
    +public void getAnnotationForArgumentToString(@SpanTag("test") Long param) {
    +}

    Running the preceding method with a value of 15 leads to setting a tag with a String value of "15".

    61. Customizations

    61.1 Customizers

    With Brave 5.7 you have various options of providing customizers for your project. Brave ships with

    • TracingCustomizer - allows configuration plugins to collaborate on building an instance of Tracing.
    • CurrentTraceContextCustomizer - allows configuration plugins to collaborate on building an instance of CurrentTraceContext.
    • ExtraFieldCustomizer - allows configuration plugins to collaborate on building an instance of ExtraFieldPropagation.Factory.

    Sleuth will search for beans of those types and automatically apply customizations.

    61.2 HTTP

    If a customization of client / server parsing of the HTTP related spans is +required, just register a bean of type brave.http.HttpClientParser or +brave.http.HttpServerParser. If client /server sampling is required, just +register a bean of type brave.sampler.SamplerFunction<HttpRequest> and name +the bean sleuthHttpClientSampler for client sampler and +sleuthHttpServerSampler for server sampler.

    For your convenience the @HttpClientSampler and @HttpServerSampler +annotations can be used to inject the proper beans or to reference the bean +names via their static String NAME fields.

    Check out Brave’s code to see an example of how to make a path-based sampler +https://github.com/openzipkin/brave/tree/master/instrumentation/http#sampling-policy

    If you want to completely rewrite the HttpTracing bean you can use the SkipPatternProvider +interface to retrieve the URL Pattern for spans that should be not sampled. Below you can see +an example of usage of SkipPatternProvider inside a server side, Sampler<HttpRequest>.

    @Configuration
    +class Config {
    +  @Bean(name = HttpServerSampler.NAME)
    +  SamplerFunction<HttpRequest> myHttpSampler(SkipPatternProvider provider) {
    +  	Pattern pattern = provider.skipPattern();
    +  	return request -> {
    +  		String url = request.path();
    +  		boolean shouldSkip = pattern.matcher(url).matches();
    +  		if (shouldSkip) {
    +  			return false;
    +  		}
    +  		return null;
    +  	};
    +  }
    +}

    61.3 TracingFilter

    You can also modify the behavior of the TracingFilter, which is the component that is responsible for processing the input HTTP request and adding tags basing on the HTTP response. +You can customize the tags or modify the response headers by registering your own instance of the TracingFilter bean.

    In the following example, we register the TracingFilter bean, add the ZIPKIN-TRACE-ID response header containing the current Span’s trace id, and add a tag with key custom and a value tag to the span.

    @Component
    +@Order(TraceWebServletAutoConfiguration.TRACING_FILTER_ORDER + 1)
    +class MyFilter extends GenericFilterBean {
    +
    +	private final Tracer tracer;
    +
    +	MyFilter(Tracer tracer) {
    +		this.tracer = tracer;
    +	}
    +
    +	@Override
    +	public void doFilter(ServletRequest request, ServletResponse response,
    +			FilterChain chain) throws IOException, ServletException {
    +		Span currentSpan = this.tracer.currentSpan();
    +		if (currentSpan == null) {
    +			chain.doFilter(request, response);
    +			return;
    +		}
    +		// for readability we're returning trace id in a hex form
    +		((HttpServletResponse) response).addHeader("ZIPKIN-TRACE-ID",
    +				currentSpan.context().traceIdString());
    +		// we can also add some custom tags
    +		currentSpan.tag("custom", "tag");
    +		chain.doFilter(request, response);
    +	}
    +
    +}

    61.4 RPC

    Sleuth automatically configures the RpcTracing bean which serves as a +foundation for RPC instrumentation such as gRPC or Dubbo.

    If a customization of client / server sampling of the RPC traces is required, +just register a bean of type brave.sampler.SamplerFunction<RpcRequest> and +name the bean sleuthRpcClientSampler for client sampler and +sleuthRpcServerSampler for server sampler.

    For your convenience the @RpcClientSampler and @RpcServerSampler +annotations can be used to inject the proper beans or to reference the bean +names via their static String NAME fields.

    Ex. Here’s a sampler that traces 100 "GetUserToken" server requests per second. +This doesn’t start new traces for requests to the health check service. Other +requests will use the global sampling configuration.

    @Configuration
    +class Config {
    +  @Bean(name = RpcServerSampler.NAME)
    +  SamplerFunction<RpcRequest> myRpcSampler() {
    +  	Matcher<RpcRequest> userAuth = and(serviceEquals("users.UserService"),
    +  			methodEquals("GetUserToken"));
    +  	return RpcRuleSampler.newBuilder()
    +  			.putRule(serviceEquals("grpc.health.v1.Health"), Sampler.NEVER_SAMPLE)
    +  			.putRule(userAuth, RateLimitingSampler.create(100)).build();
    +  }
    +}

    For more, see https://github.com/openzipkin/brave/tree/master/instrumentation/rpc#sampling-policy

    61.5 Custom service name

    By default, Sleuth assumes that, when you send a span to Zipkin, you want the span’s service name to be equal to the value of the spring.application.name property. +That is not always the case, though. +There are situations in which you want to explicitly provide a different service name for all spans coming from your application. +To achieve that, you can pass the following property to your application to override that value (the example is for a service named myService):

    spring.zipkin.service.name: myService

    61.6 Customization of Reported Spans

    Before reporting spans (for example, to Zipkin) you may want to modify that span in some way. +You can do so by using the FinishedSpanHandler interface.

    In Sleuth, we generate spans with a fixed name. +Some users want to modify the name depending on values of tags. +You can implement the FinishedSpanHandler interface to alter that name.

    The following example shows how to register two beans that implement FinishedSpanHandler:

    @Bean
    +FinishedSpanHandler handlerOne() {
    +	return new FinishedSpanHandler() {
    +		@Override
    +		public boolean handle(TraceContext traceContext, MutableSpan span) {
    +			span.name("foo");
    +			return true; // keep this span
    +		}
    +	};
    +}
    +
    +@Bean
    +FinishedSpanHandler handlerTwo() {
    +	return new FinishedSpanHandler() {
    +		@Override
    +		public boolean handle(TraceContext traceContext, MutableSpan span) {
    +			span.name(span.name() + " bar");
    +			return true; // keep this span
    +		}
    +	};
    +}

    The preceding example results in changing the name of the reported span to foo bar, just before it gets reported (for example, to Zipkin).

    61.7 Host Locator

    [Important]Important

    This section is about defining host from service discovery. +It is NOT about finding Zipkin through service discovery.

    To define the host that corresponds to a particular span, we need to resolve the host name and port. +The default approach is to take these values from server properties. +If those are not set, we try to retrieve the host name from the network interfaces.

    If you have the discovery client enabled and prefer to retrieve the host address from the registered instance in a service registry, you have to set the spring.zipkin.locator.discovery.enabled property (it is applicable for both HTTP-based and Stream-based span reporting), as follows:

    spring.zipkin.locator.discovery.enabled: true

    62. Sending Spans to Zipkin

    By default, if you add spring-cloud-starter-zipkin as a dependency to your project, when the span is closed, it is sent to Zipkin over HTTP. +The communication is asynchronous. +You can configure the URL by setting the spring.zipkin.baseUrl property, as follows:

    spring.zipkin.baseUrl: https://192.168.99.100:9411/

    If you want to find Zipkin through service discovery, you can pass the Zipkin’s service ID inside the URL, as shown in the following example for zipkinserver service ID:

    spring.zipkin.baseUrl: http://zipkinserver/

    To disable this feature just set spring.zipkin.discoveryClientEnabled to `false.

    When the Discovery Client feature is enabled, Sleuth uses +LoadBalancerClient to find the URL of the Zipkin Server. It means +that you can set up the load balancing configuration e.g. via Ribbon.

    zipkinserver:
    +  ribbon:
    +    ListOfServers: host1,host2

    If you have web, rabbit, or kafka together on the classpath, you might need to pick the means by which you would like to send spans to zipkin. +To do so, set web, rabbit, or kafka to the spring.zipkin.sender.type property. +The following example shows setting the sender type for web:

    spring.zipkin.sender.type: web

    To customize the RestTemplate that sends spans to Zipkin via HTTP, you can register +the ZipkinRestTemplateCustomizer bean.

    @Configuration
    +class MyConfig {
    +	@Bean ZipkinRestTemplateCustomizer myCustomizer() {
    +		return new ZipkinRestTemplateCustomizer() {
    +			@Override
    +			void customize(RestTemplate restTemplate) {
    +				// customize the RestTemplate
    +			}
    +		};
    +	}
    +}

    If, however, you would like to control the full process of creating the RestTemplate +object, you will have to create a bean of zipkin2.reporter.Sender type.

    	@Bean Sender myRestTemplateSender(ZipkinProperties zipkin,
    +			ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer) {
    +		RestTemplate restTemplate = mySuperCustomRestTemplate();
    +		zipkinRestTemplateCustomizer.customize(restTemplate);
    +		return myCustomSender(zipkin, restTemplate);
    +	}

    63. Zipkin Stream Span Consumer

    [Important]Important

    We recommend using Zipkin’s native support for message-based span sending. +Starting from the Edgware release, the Zipkin Stream server is deprecated. +In the Finchley release, it got removed.

    If for some reason you need to create the deprecated Stream Zipkin server, see the Dalston Documentation.

    64. Integrations

    64.1 OpenTracing

    Spring Cloud Sleuth is compatible with OpenTracing. +If you have OpenTracing on the classpath, we automatically register the OpenTracing Tracer bean. +If you wish to disable this, set spring.sleuth.opentracing.enabled to false

    64.2 Runnable and Callable

    If you wrap your logic in Runnable or Callable, you can wrap those classes in their Sleuth representative, as shown in the following example for Runnable:

    Runnable runnable = new Runnable() {
    +	@Override
    +	public void run() {
    +		// do some work
    +	}
    +
    +	@Override
    +	public String toString() {
    +		return "spanNameFromToStringMethod";
    +	}
    +};
    +// Manual `TraceRunnable` creation with explicit "calculateTax" Span name
    +Runnable traceRunnable = new TraceRunnable(this.tracing, spanNamer, runnable,
    +		"calculateTax");
    +// Wrapping `Runnable` with `Tracing`. That way the current span will be available
    +// in the thread of `Runnable`
    +Runnable traceRunnableFromTracer = this.tracing.currentTraceContext()
    +		.wrap(runnable);

    The following example shows how to do so for Callable:

    Callable<String> callable = new Callable<String>() {
    +	@Override
    +	public String call() throws Exception {
    +		return someLogic();
    +	}
    +
    +	@Override
    +	public String toString() {
    +		return "spanNameFromToStringMethod";
    +	}
    +};
    +// Manual `TraceCallable` creation with explicit "calculateTax" Span name
    +Callable<String> traceCallable = new TraceCallable<>(this.tracing, spanNamer,
    +		callable, "calculateTax");
    +// Wrapping `Callable` with `Tracing`. That way the current span will be available
    +// in the thread of `Callable`
    +Callable<String> traceCallableFromTracer = this.tracing.currentTraceContext()
    +		.wrap(callable);

    That way, you ensure that a new span is created and closed for each execution.

    64.3 Hystrix

    64.3.1 Custom Concurrency Strategy

    We register a custom HystrixConcurrencyStrategy called TraceCallable that wraps all Callable instances in their Sleuth representative. +The strategy either starts or continues a span, depending on whether tracing was already going on before the Hystrix command was called. +To disable the custom Hystrix Concurrency Strategy, set the spring.sleuth.hystrix.strategy.enabled to false.

    64.3.2 Manual Command setting

    Assume that you have the following HystrixCommand:

    HystrixCommand<String> hystrixCommand = new HystrixCommand<String>(setter) {
    +	@Override
    +	protected String run() throws Exception {
    +		return someLogic();
    +	}
    +};

    To pass the tracing information, you have to wrap the same logic in the Sleuth version of the HystrixCommand, which is called +TraceCommand, as shown in the following example:

    TraceCommand<String> traceCommand = new TraceCommand<String>(tracer, setter) {
    +	@Override
    +	public String doRun() throws Exception {
    +		return someLogic();
    +	}
    +};

    64.4 RxJava

    We registering a custom RxJavaSchedulersHook that wraps all Action0 instances in their Sleuth representative, which is called TraceAction. +The hook either starts or continues a span, depending on whether tracing was already going on before the Action was scheduled. +To disable the custom RxJavaSchedulersHook, set the spring.sleuth.rxjava.schedulers.hook.enabled to false.

    You can define a list of regular expressions for thread names for which you do not want spans to be created. +To do so, provide a comma-separated list of regular expressions in the spring.sleuth.rxjava.schedulers.ignoredthreads property.

    [Important]Important

    The suggest approach to reactive programming and Sleuth is to use +the Reactor support.

    64.5 HTTP integration

    Features from this section can be disabled by setting the spring.sleuth.web.enabled property with value equal to false.

    64.5.1 HTTP Filter

    Through the TracingFilter, all sampled incoming requests result in creation of a Span. +That Span’s name is http: + the path to which the request was sent. +For example, if the request was sent to /this/that then the name will be http:/this/that. +You can configure which URIs you would like to skip by setting the spring.sleuth.web.skipPattern property. +If you have ManagementServerProperties on classpath, its value of contextPath gets appended to the provided skip pattern. +If you want to reuse the Sleuth’s default skip patterns and just append your own, pass those patterns by using the spring.sleuth.web.additionalSkipPattern.

    By default, all the spring boot actuator endpoints are automatically added to the skip pattern. +If you want to disable this behaviour set spring.sleuth.web.ignore-auto-configured-skip-patterns +to true.

    To change the order of tracing filter registration, please set the +spring.sleuth.web.filter-order property.

    To disable the filter that logs uncaught exceptions you can disable the +spring.sleuth.web.exception-throwing-filter-enabled property.

    64.5.2 HandlerInterceptor

    Since we want the span names to be precise, we use a TraceHandlerInterceptor that either wraps an existing HandlerInterceptor or is added directly to the list of existing HandlerInterceptors. +The TraceHandlerInterceptor adds a special request attribute to the given HttpServletRequest. +If the the TracingFilter does not see this attribute, it creates a fallback span, which is an additional span created on the server side so that the trace is presented properly in the UI. +If that happens, there is probably missing instrumentation. +In that case, please file an issue in Spring Cloud Sleuth.

    64.5.3 Async Servlet support

    If your controller returns a Callable or a WebAsyncTask, Spring Cloud Sleuth continues the existing span instead of creating a new one.

    64.5.4 WebFlux support

    Through TraceWebFilter, all sampled incoming requests result in creation of a Span. +That Span’s name is http: + the path to which the request was sent. +For example, if the request was sent to /this/that, the name is http:/this/that. +You can configure which URIs you would like to skip by using the spring.sleuth.web.skipPattern property. +If you have ManagementServerProperties on the classpath, its value of contextPath gets appended to the provided skip pattern. +If you want to reuse Sleuth’s default skip patterns and append your own, pass those patterns by using the spring.sleuth.web.additionalSkipPattern.

    To change the order of tracing filter registration, please set the +spring.sleuth.web.filter-order property.

    64.5.5 Dubbo RPC support

    Via the integration with Brave, Spring Cloud Sleuth supports Dubbo. +It’s enough to add the brave-instrumentation-dubbo dependency:

    <dependency>
    +    <groupId>io.zipkin.brave</groupId>
    +    <artifactId>brave-instrumentation-dubbo</artifactId>
    +</dependency>

    You need to also set a dubbo.properties file with the following contents:

    dubbo.provider.filter=tracing
    +dubbo.consumer.filter=tracing

    You can read more about Brave - Dubbo integration here. +An example of Spring Cloud Sleuth and Dubbo can be found here.

    64.6 HTTP Client Integration

    64.6.1 Synchronous Rest Template

    We inject a RestTemplate interceptor to ensure that all the tracing information is passed to the requests. +Each time a call is made, a new Span is created. +It gets closed upon receiving the response. +To block the synchronous RestTemplate features, set spring.sleuth.web.client.enabled to false.

    [Important]Important

    You have to register RestTemplate as a bean so that the interceptors get injected. +If you create a RestTemplate instance with a new keyword, the instrumentation does NOT work.

    64.6.2 Asynchronous Rest Template

    [Important]Important

    Starting with Sleuth 2.0.0, we no longer register a bean of AsyncRestTemplate type. +It is up to you to create such a bean. +Then we instrument it.

    To block the AsyncRestTemplate features, set spring.sleuth.web.async.client.enabled to false. +To disable creation of the default TraceAsyncClientHttpRequestFactoryWrapper, set spring.sleuth.web.async.client.factory.enabled +to false. +If you do not want to create AsyncRestClient at all, set spring.sleuth.web.async.client.template.enabled to false.

    Multiple Asynchronous Rest Templates

    Sometimes you need to use multiple implementations of the Asynchronous Rest Template. +In the following snippet, you can see an example of how to set up such a custom AsyncRestTemplate:

    @Configuration
    +@EnableAutoConfiguration
    +static class Config {
    +
    +	@Bean(name = "customAsyncRestTemplate")
    +	public AsyncRestTemplate traceAsyncRestTemplate() {
    +		return new AsyncRestTemplate(asyncClientFactory(),
    +				clientHttpRequestFactory());
    +	}
    +
    +	private ClientHttpRequestFactory clientHttpRequestFactory() {
    +		ClientHttpRequestFactory clientHttpRequestFactory = new CustomClientHttpRequestFactory();
    +		// CUSTOMIZE HERE
    +		return clientHttpRequestFactory;
    +	}
    +
    +	private AsyncClientHttpRequestFactory asyncClientFactory() {
    +		AsyncClientHttpRequestFactory factory = new CustomAsyncClientHttpRequestFactory();
    +		// CUSTOMIZE HERE
    +		return factory;
    +	}
    +
    +}

    64.6.3 WebClient

    We inject a ExchangeFilterFunction implementation that creates a span and, through on-success and on-error callbacks, takes care of closing client-side spans.

    To block this feature, set spring.sleuth.web.client.enabled to false.

    [Important]Important

    You have to register WebClient as a bean so that the tracing instrumentation gets applied. +If you create a WebClient instance with a new keyword, the instrumentation does NOT work.

    64.6.4 Traverson

    If you use the Traverson library, you can inject a RestTemplate as a bean into your Traverson object. +Since RestTemplate is already intercepted, you get full support for tracing in your client. The following pseudo code +shows how to do that:

    @Autowired RestTemplate restTemplate;
    +
    +Traverson traverson = new Traverson(URI.create("http://some/address"),
    +    MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8).setRestOperations(restTemplate);
    +// use Traverson

    64.6.5 Apache HttpClientBuilder and HttpAsyncClientBuilder

    We instrument the HttpClientBuilder and HttpAsyncClientBuilder so that +tracing context gets injected to the sent requests.

    To block these features, set spring.sleuth.web.client.enabled to false.

    64.6.6 Netty HttpClient

    We instrument the Netty’s HttpClient.

    To block this feature, set spring.sleuth.web.client.enabled to false.

    [Important]Important

    You have to register HttpClient as a bean so that the instrumentation happens. +If you create a HttpClient instance with a new keyword, the instrumentation does NOT work.

    64.6.7 UserInfoRestTemplateCustomizer

    We instrument the Spring Security’s UserInfoRestTemplateCustomizer.

    To block this feature, set spring.sleuth.web.client.enabled to false.

    64.7 Feign

    By default, Spring Cloud Sleuth provides integration with Feign through TraceFeignClientAutoConfiguration. +You can disable it entirely by setting spring.sleuth.feign.enabled to false. +If you do so, no Feign-related instrumentation take place.

    Part of Feign instrumentation is done through a FeignBeanPostProcessor. +You can disable it by setting spring.sleuth.feign.processor.enabled to false. +If you set it to false, Spring Cloud Sleuth does not instrument any of your custom Feign components. +However, all the default instrumentation is still there.

    64.8 gRPC

    Spring Cloud Sleuth provides instrumentation for gRPC through TraceGrpcAutoConfiguration. You can disable it entirely by setting spring.sleuth.grpc.enabled to false.

    64.8.1 Variant 1

    Dependencies

    [Important]Important

    The gRPC integration relies on two external libraries to instrument clients and servers and both of those libraries must be on the class path to enable the instrumentation.

    Maven:

    		<dependency>
    +			<groupId>io.github.lognet</groupId>
    +			<artifactId>grpc-spring-boot-starter</artifactId>
    +		</dependency>
    +		<dependency>
    +			<groupId>io.zipkin.brave</groupId>
    +			<artifactId>brave-instrumentation-grpc</artifactId>
    +		</dependency>

    Gradle:

        compile("io.github.lognet:grpc-spring-boot-starter")
    +    compile("io.zipkin.brave:brave-instrumentation-grpc")

    Server Instrumentation

    Spring Cloud Sleuth leverages grpc-spring-boot-starter to register Brave’s gRPC server interceptor with all services annotated with @GRpcService.

    Client Instrumentation

    gRPC clients leverage a ManagedChannelBuilder to construct a ManagedChannel used to communicate to the gRPC server. The native ManagedChannelBuilder provides static methods as entry points for construction of ManagedChannel instances, however, this mechanism is outside the influence of the Spring application context.

    [Important]Important

    Spring Cloud Sleuth provides a SpringAwareManagedChannelBuilder that can be customized through the Spring application context and injected by gRPC clients. This builder must be used when creating ManagedChannel instances.

    Sleuth creates a TracingManagedChannelBuilderCustomizer which inject Brave’s client interceptor into the SpringAwareManagedChannelBuilder.

    64.8.2 Variant 2

    Grpc Spring Boot Starter automatically detects the presence of Spring Cloud Sleuth and brave’s instrumentation for gRPC and registers the necessary client and/or server tooling.

    64.9 Asynchronous Communication

    64.9.1 @Async Annotated methods

    In Spring Cloud Sleuth, we instrument async-related components so that the tracing information is passed between threads. +You can disable this behavior by setting the value of spring.sleuth.async.enabled to false.

    If you annotate your method with @Async, we automatically create a new Span with the following characteristics:

    • If the method is annotated with @SpanName, the value of the annotation is the Span’s name.
    • If the method is not annotated with @SpanName, the Span name is the annotated method name.
    • The span is tagged with the method’s class name and method name.

    64.9.2 @Scheduled Annotated Methods

    In Spring Cloud Sleuth, we instrument scheduled method execution so that the tracing information is passed between threads. +You can disable this behavior by setting the value of spring.sleuth.scheduled.enabled to false.

    If you annotate your method with @Scheduled, we automatically create a new span with the following characteristics:

    • The span name is the annotated method name.
    • The span is tagged with the method’s class name and method name.

    If you want to skip span creation for some @Scheduled annotated classes, you can set the spring.sleuth.scheduled.skipPattern with a regular expression that matches the fully qualified name of the @Scheduled annotated class. +If you use spring-cloud-sleuth-stream and spring-cloud-netflix-hystrix-stream together, a span is created for each Hystrix metrics and sent to Zipkin. +This behavior may be annoying. That’s why, by default, spring.sleuth.scheduled.skipPattern=org.springframework.cloud.netflix.hystrix.stream.HystrixStreamTask.

    64.9.3 Executor, ExecutorService, and ScheduledExecutorService

    We provide LazyTraceExecutor, TraceableExecutorService, and TraceableScheduledExecutorService. Those implementations create spans each time a new task is submitted, invoked, or scheduled.

    The following example shows how to pass tracing information with TraceableExecutorService when working with CompletableFuture:

    CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> {
    +	// perform some logic
    +	return 1_000_000L;
    +}, new TraceableExecutorService(beanFactory, executorService,
    +		// 'calculateTax' explicitly names the span - this param is optional
    +		"calculateTax"));
    [Important]Important

    Sleuth does not work with parallelStream() out of the box. +If you want to have the tracing information propagated through the stream, you have to use the approach with supplyAsync(…​), as shown earlier.

    If there are beans that implement the Executor interface that you would like +to exclude from span creation, you can use the spring.sleuth.async.ignored-beans +property where you can provide a list of bean names.

    Customization of Executors

    Sometimes, you need to set up a custom instance of the AsyncExecutor. +The following example shows how to set up such a custom Executor:

    @Configuration
    +@EnableAutoConfiguration
    +@EnableAsync
    +// add the infrastructure role to ensure that the bean gets auto-proxied
    +@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    +static class CustomExecutorConfig extends AsyncConfigurerSupport {
    +
    +	@Autowired
    +	BeanFactory beanFactory;
    +
    +	@Override
    +	public Executor getAsyncExecutor() {
    +		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    +		// CUSTOMIZE HERE
    +		executor.setCorePoolSize(7);
    +		executor.setMaxPoolSize(42);
    +		executor.setQueueCapacity(11);
    +		executor.setThreadNamePrefix("MyExecutor-");
    +		// DON'T FORGET TO INITIALIZE
    +		executor.initialize();
    +		return new LazyTraceExecutor(this.beanFactory, executor);
    +	}
    +
    +}
    [Tip]Tip

    To ensure that your configuration gets post processed, remember +to add the @Role(BeanDefinition.ROLE_INFRASTRUCTURE) on your +@Configuration class

    64.10 Messaging

    Features from this section can be disabled by setting the spring.sleuth.messaging.enabled property with value equal to false.

    64.10.1 Spring Integration and Spring Cloud Stream

    Spring Cloud Sleuth integrates with Spring Integration. +It creates spans for publish and subscribe events. +To disable Spring Integration instrumentation, set spring.sleuth.integration.enabled to false.

    You can provide the spring.sleuth.integration.patterns pattern to explicitly provide the names of channels that you want to include for tracing. +By default, all channels but hystrixStreamOutput channel are included.

    [Important]Important

    When using the Executor to build a Spring Integration IntegrationFlow, you must use the untraced version of the Executor. +Decorating the Spring Integration Executor Channel with TraceableExecutorService causes the spans to be improperly closed.

    If you want to customize the way tracing context is read from and written to message headers, +it’s enough for you to register beans of types:

    • Propagation.Setter<MessageHeaderAccessor, String> - for writing headers to the message
    • Propagation.Getter<MessageHeaderAccessor, String> - for reading headers from the message

    64.10.2 Spring RabbitMq

    We instrument the RabbitTemplate so that tracing headers get injected +into the message.

    To block this feature, set spring.sleuth.messaging.rabbit.enabled to false.

    64.10.3 Spring Kafka

    We instrument the Spring Kafka’s ProducerFactory and ConsumerFactory +so that tracing headers get injected into the created Spring Kafka’s +Producer and Consumer.

    To block this feature, set spring.sleuth.messaging.kafka.enabled to false.

    64.10.4 Spring JMS

    We instrument the JmsTemplate so that tracing headers get injected +into the message. We also support @JmsListener annotated methods on the consumer side.

    To block this feature, set spring.sleuth.messaging.jms.enabled to false.

    [Important]Important

    We don’t support baggage propagation for JMS

    64.11 Zuul

    We instrument the Zuul Ribbon integration by enriching the Ribbon requests with tracing information. +To disable Zuul support, set the spring.sleuth.zuul.enabled property to false.

    64.12 Project Reactor

    For projects depending on Project Reactor such as Spring Cloud Gateway, we suggest turning the spring.sleuth.reactor.decorate-on-each option to false. That way an increased performance gain should be observed in comparison to the standard instrumentation mechanism. What this option does is it will wrap decorate onLast operator instead of onEach which will result in creation of far fewer objects. The downside of this is that when Project Reactor will change threads, the trace propagation will continue without issues, however anything relying on the ThreadLocal such as e.g. MDC entries can be buggy.

    65. Running examples

    You can see the running examples deployed in the Pivotal Web Services. +Check them out at the following links:

    Part IX. Spring Cloud Consul

    Greenwich.SR5

    This project provides Consul integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms. With a few +simple annotations you can quickly enable and configure the common patterns inside your +application and build large distributed systems with Consul based components. The +patterns provided include Service Discovery, Control Bus and Configuration. +Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon), Circuit Breaker +(Hystrix) are provided by integration with Spring Cloud Netflix.

    66. Install Consul

    Please see the installation documentation for instructions on how to install Consul.

    67. Consul Agent

    A Consul Agent client must be available to all Spring Cloud Consul applications. By default, the Agent client is expected to be at localhost:8500. See the Agent documentation for specifics on how to start an Agent client and how to connect to a cluster of Consul Agent Servers. For development, after you have installed consul, you may start a Consul Agent using the following command:

    ./src/main/bash/local_run_consul.sh

    This will start an agent in server mode on port 8500, with the ui available at http://localhost:8500

    68. Service Discovery with Consul

    Service Discovery is one of the key tenets of a microservice based architecture. Trying to hand configure each client or some form of convention can be very difficult to do and can be very brittle. Consul provides Service Discovery services via an HTTP API and DNS. Spring Cloud Consul leverages the HTTP API for service registration and discovery. This does not prevent non-Spring Cloud applications from leveraging the DNS interface. Consul Agents servers are run in a cluster that communicates via a gossip protocol and uses the Raft consensus protocol.

    68.1 How to activate

    To activate Consul Service Discovery use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-discovery. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    68.2 Registering with Consul

    When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An HTTP Check is created by default that Consul hits the /health endpoint every 10 seconds. If the health check fails, the service instance is marked as critical.

    Example Consul client:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello world";
    +    }
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}

    (i.e. utterly normal Spring Boot app). If the Consul client is located somewhere other than localhost:8500, the configuration is required to locate the client. Example:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      host: localhost
    +      port: 8500

    +

    [Caution]Caution

    If you use Spring Cloud Consul Config, the above values will need to be placed in bootstrap.yml instead of application.yml.

    The default service name, instance id and port, taken from the Environment, are ${spring.application.name}, the Spring Context ID and ${server.port} respectively.

    To disable the Consul Discovery Client you can set spring.cloud.consul.discovery.enabled to false. Consul Discovery Client will also be disabled when spring.cloud.discovery.enabled is set to false.

    To disable the service registration you can set spring.cloud.consul.discovery.register to false.

    68.2.1 Registering Management as a Separate Service

    When management server port is set to something different than the application port, by setting management.server.port property, management service will be registered as a separate service than the application service. For example:

    application.yml.  +

    spring:
    +  application:
    +    name: myApp
    +management:
    +  server:
    +    port: 4452

    +

    Above configuration will register following 2 services:

    • Application Service:
    ID: myApp
    +Name: myApp
    • Management Service:
    ID: myApp-management
    +Name: myApp-management

    Management service will inherit its instanceId and serviceName from the application service. For example:

    application.yml.  +

    spring:
    +  application:
    +    name: myApp
    +management:
    +  server:
    +    port: 4452
    +spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        instance-id: custom-service-id
    +        serviceName: myprefix-${spring.application.name}

    +

    Above configuration will register following 2 services:

    • Application Service:
    ID: custom-service-id
    +Name: myprefix-myApp
    • Management Service:
    ID: custom-service-id-management
    +Name: myprefix-myApp-management

    Further customization is possible via following properties:

    /** Port to register the management service under (defaults to management port) */
    +spring.cloud.consul.discovery.management-port
    +
    +/** Suffix to use when registering management service (defaults to "management" */
    +spring.cloud.consul.discovery.management-suffix
    +
    +/** Tags to use when registering management service (defaults to "management" */
    +spring.cloud.consul.discovery.management-tags

    68.3 HTTP Health Check

    The health check for a Consul instance defaults to "/health", which is the default locations of a useful endpoint in a Spring Boot Actuator application. You need to change these, even for an Actuator application if you use a non-default context path or servlet path (e.g. server.servletPath=/foo) or management endpoint path (e.g. management.server.servlet.context-path=/admin). The interval that Consul uses to check the health endpoint may also be configured. "10s" and "1m" represent 10 seconds and 1 minute respectively. Example:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        healthCheckPath: ${management.server.servlet.context-path}/health
    +        healthCheckInterval: 15s

    +

    You can disable the health check by setting management.health.consul.enabled=false.

    68.3.1 Metadata and Consul tags

    Consul does not yet support metadata on services. Spring Cloud’s ServiceInstance has a Map<String, String> metadata field. Spring Cloud Consul uses Consul tags to approximate metadata until Consul officially supports metadata. Tags with the form key=value will be split and used as a Map key and value respectively. Tags without the equal = sign, will be used as both the key and value.

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        tags: foo=bar, baz

    +

    The above configuration will result in a map with foo→bar and baz→baz.

    68.3.2 Making the Consul Instance ID Unique

    By default a consul instance is registered with an ID that is equal to its Spring Application Context ID. By default, the Spring Application Context ID is ${spring.application.name}:comma,separated,profiles:${server.port}. For most cases, this will allow multiple instances of one service to run on one machine. If further uniqueness is required, Using Spring Cloud you can override this by providing a unique identifier in spring.cloud.consul.discovery.instanceId. For example:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}

    +

    With this metadata, and multiple service instances deployed on localhost, the random value will kick in there to make the instance unique. In Cloudfoundry the vcap.application.instance_id will be populated automatically in a Spring Boot application, so the random value will not be needed.

    68.3.3 Applying Headers to Health Check Requests

    Headers can be applied to health check requests. For example, if you’re trying to register a Spring Cloud Config server that uses Vault Backend:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        health-check-headers:
    +          X-Config-Token: 6442e58b-d1ea-182e-cfa5-cf9cddef0722

    +

    According to the HTTP standard, each header can have more than one values, in which case, an array can be supplied:

    application.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      discovery:
    +        health-check-headers:
    +          X-Config-Token:
    +            - "6442e58b-d1ea-182e-cfa5-cf9cddef0722"
    +            - "Some other value"

    +

    68.4 Looking up services

    68.4.1 Using Ribbon

    Spring Cloud has support for Feign (a REST client builder) and also Spring RestTemplate +for looking up services using the logical service names/ids instead of physical URLs. Both Feign and the discovery-aware RestTemplate utilize Ribbon for client-side load balancing.

    If you want to access service STORES using the RestTemplate simply declare:

    @LoadBalanced
    +@Bean
    +public RestTemplate loadbalancedRestTemplate() {
    +     new RestTemplate();
    +}

    and use it like this (notice how we use the STORES service name/id from Consul instead of a fully qualified domainname):

    @Autowired
    +RestTemplate restTemplate;
    +
    +public String getFirstProduct() {
    +   return this.restTemplate.getForObject("https://STORES/products/1", String.class);
    +}

    If you have Consul clusters in multiple datacenters and you want to access a service in another datacenter a service name/id alone is not enough. In that case +you use property spring.cloud.consul.discovery.datacenters.STORES=dc-west where STORES is the service name/id and dc-west is the datacenter +where the STORES service lives.

    68.4.2 Using the DiscoveryClient

    You can also use the org.springframework.cloud.client.discovery.DiscoveryClient which provides a simple API for discovery clients that is not specific to Netflix, e.g.

    @Autowired
    +private DiscoveryClient discoveryClient;
    +
    +public String serviceUrl() {
    +    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    +    if (list != null && list.size() > 0 ) {
    +        return list.get(0).getUri();
    +    }
    +    return null;
    +}

    68.5 Consul Catalog Watch

    The Consul Catalog Watch takes advantage of the ability of consul to watch services. The Catalog Watch makes a blocking Consul HTTP API call to determine if any services have changed. If there is new service data a Heartbeat Event is published.

    To change the frequency of when the Config Watch is called change spring.cloud.consul.config.discovery.catalog-services-watch-delay. The default value is 1000, which is in milliseconds. The delay is the amount of time after the end of the previous invocation and the start of the next.

    To disable the Catalog Watch set spring.cloud.consul.discovery.catalogServicesWatch.enabled=false.

    The watch uses a Spring TaskScheduler to schedule the call to consul. By default it is a ThreadPoolTaskScheduler with a poolSize of 1. To change the TaskScheduler, create a bean of type TaskScheduler named with the ConsulDiscoveryClientConfiguration.CATALOG_WATCH_TASK_SCHEDULER_NAME constant.

    69. Distributed Configuration with Consul

    Consul provides a Key/Value Store for storing configuration and other metadata. Spring Cloud Consul Config is an alternative to the Config Server and Client. Configuration is loaded into the Spring Environment during the special "bootstrap" phase. Configuration is stored in the /config folder by default. Multiple PropertySource instances are created based on the application’s name and the active profiles that mimicks the Spring Cloud Config order of resolving properties. For example, an application with the name "testApp" and with the "dev" profile will have the following property sources created:

    config/testApp,dev/
    +config/testApp/
    +config/application,dev/
    +config/application/

    The most specific property source is at the top, with the least specific at the bottom. Properties in the config/application folder are applicable to all applications using consul for configuration. Properties in the config/testApp folder are only available to the instances of the service named "testApp".

    Configuration is currently read on startup of the application. Sending a HTTP POST to /refresh will cause the configuration to be reloaded. Section 69.3, “Config Watch” will also automatically detect changes and reload the application context.

    69.1 How to activate

    To get started with Consul Configuration use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-config. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    This will enable auto-configuration that will setup Spring Cloud Consul Config.

    69.2 Customizing

    Consul Config may be customized using the following properties:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      config:
    +        enabled: true
    +        prefix: configuration
    +        defaultContext: apps
    +        profileSeparator: '::'

    +

    • enabled setting this value to "false" disables Consul Config
    • prefix sets the base folder for configuration values
    • defaultContext sets the folder name used by all applications
    • profileSeparator sets the value of the separator used to separate the profile name in property sources with profiles

    69.3 Config Watch

    The Consul Config Watch takes advantage of the ability of consul to watch a key prefix. The Config Watch makes a blocking Consul HTTP API call to determine if any relevant configuration data has changed for the current application. If there is new configuration data a Refresh Event is published. This is equivalent to calling the /refresh actuator endpoint.

    To change the frequency of when the Config Watch is called change spring.cloud.consul.config.watch.delay. The default value is 1000, which is in milliseconds. The delay is the amount of time after the end of the previous invocation and the start of the next.

    To disable the Config Watch set spring.cloud.consul.config.watch.enabled=false.

    The watch uses a Spring TaskScheduler to schedule the call to consul. By default it is a ThreadPoolTaskScheduler with a poolSize of 1. To change the TaskScheduler, create a bean of type TaskScheduler named with the ConsulConfigAutoConfiguration.CONFIG_WATCH_TASK_SCHEDULER_NAME constant.

    69.4 YAML or Properties with Config

    It may be more convenient to store a blob of properties in YAML or Properties format as opposed to individual key/value pairs. Set the spring.cloud.consul.config.format property to YAML or PROPERTIES. For example to use YAML:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      config:
    +        format: YAML

    +

    YAML must be set in the appropriate data key in consul. Using the defaults above the keys would look like:

    config/testApp,dev/data
    +config/testApp/data
    +config/application,dev/data
    +config/application/data

    You could store a YAML document in any of the keys listed above.

    You can change the data key using spring.cloud.consul.config.data-key.

    69.5 git2consul with Config

    git2consul is a Consul community project that loads files from a git repository to individual keys into Consul. By default the names of the keys are names of the files. YAML and Properties files are supported with file extensions of .yml and .properties respectively. Set the spring.cloud.consul.config.format property to FILES. For example:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    consul:
    +      config:
    +        format: FILES

    +

    Given the following keys in /config, the development profile and an application name of foo:

    .gitignore
    +application.yml
    +bar.properties
    +foo-development.properties
    +foo-production.yml
    +foo.properties
    +master.ref

    the following property sources would be created:

    config/foo-development.properties
    +config/foo.properties
    +config/application.yml

    The value of each key needs to be a properly formatted YAML or Properties file.

    69.6 Fail Fast

    It may be convenient in certain circumstances (like local development or certain test scenarios) to not fail if consul isn’t available for configuration. Setting spring.cloud.consul.config.failFast=false in bootstrap.yml will cause the configuration module to log a warning rather than throw an exception. This will allow the application to continue startup normally.

    70. Consul Retry

    If you expect that the consul agent may occasionally be unavailable when +your app starts, you can ask it to keep trying after a failure. You need to add +spring-retry and spring-boot-starter-aop to your classpath. The default +behaviour is to retry 6 times with an initial backoff interval of 1000ms and an +exponential multiplier of 1.1 for subsequent backoffs. You can configure these +properties (and others) using spring.cloud.consul.retry.* configuration properties. +This works with both Spring Cloud Consul Config and Discovery registration.

    [Tip]Tip

    To take full control of the retry add a @Bean of type +RetryOperationsInterceptor with id "consulRetryInterceptor". Spring +Retry has a RetryInterceptorBuilder that makes it easy to create one.

    71. Spring Cloud Bus with Consul

    71.1 How to activate

    To get started with the Consul Bus use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-bus. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.

    See the Spring Cloud Bus documentation for the available actuator endpoints and howto send custom messages.

    72. Circuit Breaker with Hystrix

    Applications can use the Hystrix Circuit Breaker provided by the Spring Cloud Netflix project by including this starter in the projects pom.xml: spring-cloud-starter-hystrix. Hystrix doesn’t depend on the Netflix Discovery Client. The @EnableHystrix annotation should be placed on a configuration class (usually the main class). Then methods can be annotated with @HystrixCommand to be protected by a circuit breaker. See the documentation for more details.

    73. Hystrix metrics aggregation with Turbine and Consul

    Turbine (provided by the Spring Cloud Netflix project), aggregates multiple instances Hystrix metrics streams, so the dashboard can display an aggregate view. Turbine uses the DiscoveryClient interface to lookup relevant instances. To use Turbine with Spring Cloud Consul, configure the Turbine application in a manner similar to the following examples:

    pom.xml.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-netflix-turbine</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    +</dependency>

    +

    Notice that the Turbine dependency is not a starter. The turbine starter includes support for Netflix Eureka.

    application.yml.  +

    spring.application.name: turbine
    +applications: consulhystrixclient
    +turbine:
    +  aggregator:
    +    clusterConfig: ${applications}
    +  appConfig: ${applications}

    +

    The clusterConfig and appConfig sections must match, so it’s useful to put the comma-separated list of service ID’s into a separate configuration property.

    Turbine.java.  +

    @EnableTurbine
    +@SpringBootApplication
    +public class Turbine {
    +    public static void main(String[] args) {
    +        SpringApplication.run(DemoturbinecommonsApplication.class, args);
    +    }
    +}

    +

    Part X. Spring Cloud Zookeeper

    This project provides Zookeeper integrations for Spring Boot applications through +autoconfiguration and binding to the Spring Environment and other Spring programming model +idioms. With a few annotations, you can quickly enable and configure the common patterns +inside your application and build large distributed systems with Zookeeper based +components. The provided patterns include Service Discovery and Configuration. Integration +with Spring Cloud Netflix provides Intelligent Routing (Zuul), Client Side Load Balancing +(Ribbon), and Circuit Breaker (Hystrix).

    74. Install Zookeeper

    See the installation +documentation for instructions on how to install Zookeeper.

    Spring Cloud Zookeeper uses Apache Curator behind the scenes. +While Zookeeper 3.5.x is still considered "beta" by the Zookeeper development team, +the reality is that it is used in production by many users. +However, Zookeeper 3.4.x is also used in production. +Prior to Apache Curator 4.0, both versions of Zookeeper were supported via two versions of Apache Curator. +Starting with Curator 4.0 both versions of Zookeeper are supported via the same Curator libraries.

    In case you are integrating with version 3.4 you need to change the Zookeeper dependency +that comes shipped with curator, and thus spring-cloud-zookeeper. +To do so simply exclude that dependency and add the 3.4.x version like shown below.

    maven.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-zookeeper-all</artifactId>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>org.apache.zookeeper</groupId>
    +            <artifactId>zookeeper</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>
    +<dependency>
    +    <groupId>org.apache.zookeeper</groupId>
    +    <artifactId>zookeeper</artifactId>
    +    <version>3.4.12</version>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>org.slf4j</groupId>
    +            <artifactId>slf4j-log4j12</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>

    +

    gradle.  +

    compile('org.springframework.cloud:spring-cloud-starter-zookeeper-all') {
    +  exclude group: 'org.apache.zookeeper', module: 'zookeeper'
    +}
    +compile('org.apache.zookeeper:zookeeper:3.4.12') {
    +  exclude group: 'org.slf4j', module: 'slf4j-log4j12'
    +}

    +

    75. Service Discovery with Zookeeper

    Service Discovery is one of the key tenets of a microservice based architecture. Trying to +hand-configure each client or some form of convention can be difficult to do and can be +brittle. Curator(A Java library for Zookeeper) provides Service +Discovery through a Service Discovery +Extension. Spring Cloud Zookeeper uses this extension for service registration and +discovery.

    75.1 Activating

    Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-discovery enables +autoconfiguration that sets up Spring Cloud Zookeeper Discovery.

    [Note]Note

    For web functionality, you still need to include +org.springframework.boot:spring-boot-starter-web.

    [Caution]Caution

    When working with version 3.4 of Zookeeper you need to change +the way you include the dependency as described here.

    75.2 Registering with Zookeeper

    When a client registers with Zookeeper, it provides metadata (such as host and port, ID, +and name) about itself.

    The following example shows a Zookeeper client:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello world";
    +    }
    +
    +    public static void main(String[] args) {
    +        new SpringApplicationBuilder(Application.class).web(true).run(args);
    +    }
    +
    +}
    [Note]Note

    The preceding example is a normal Spring Boot application.

    If Zookeeper is located somewhere other than localhost:2181, the configuration must +provide the location of the server, as shown in the following example:

    application.yml.  +

    spring:
    +  cloud:
    +    zookeeper:
    +      connect-string: localhost:2181

    +

    [Caution]Caution

    If you use Spring Cloud Zookeeper Config, the +values shown in the preceding example need to be in bootstrap.yml instead of +application.yml.

    The default service name, instance ID, and port (taken from the Environment) are +${spring.application.name}, the Spring Context ID, and ${server.port}, respectively.

    Having spring-cloud-starter-zookeeper-discovery on the classpath makes the app into both +a Zookeeper service (that is, it registers itself) and a client (that is, it can +query Zookeeper to locate other services).

    If you would like to disable the Zookeeper Discovery Client, you can set +spring.cloud.zookeeper.discovery.enabled to false.

    75.3 Using the DiscoveryClient

    Spring Cloud has support for +Feign +(a REST client builder) and +Spring +RestTemplate, using logical service names instead of physical URLs.

    You can also use the org.springframework.cloud.client.discovery.DiscoveryClient, which +provides a simple API for discovery clients that is not specific to Netflix, as shown in +the following example:

    @Autowired
    +private DiscoveryClient discoveryClient;
    +
    +public String serviceUrl() {
    +    List<ServiceInstance> list = discoveryClient.getInstances("STORES");
    +    if (list != null && list.size() > 0 ) {
    +        return list.get(0).getUri().toString();
    +    }
    +    return null;
    +}

    76. Using Spring Cloud Zookeeper with Spring Cloud Netflix Components

    Spring Cloud Netflix supplies useful tools that work regardless of which DiscoveryClient +implementation you use. Feign, Turbine, Ribbon, and Zuul all work with Spring Cloud +Zookeeper.

    76.1 Ribbon with Zookeeper

    Spring Cloud Zookeeper provides an implementation of Ribbon’s ServerList. When you use +the spring-cloud-starter-zookeeper-discovery, Ribbon is autoconfigured to use the +ZookeeperServerList by default.

    77. Spring Cloud Zookeeper and Service Registry

    Spring Cloud Zookeeper implements the ServiceRegistry interface, letting developers +register arbitrary services in a programmatic way.

    The ServiceInstanceRegistration class offers a builder() method to create a +Registration object that can be used by the ServiceRegistry, as shown in the following +example:

    @Autowired
    +private ZookeeperServiceRegistry serviceRegistry;
    +
    +public void registerThings() {
    +    ZookeeperRegistration registration = ServiceInstanceRegistration.builder()
    +            .defaultUriSpec()
    +            .address("anyUrl")
    +            .port(10)
    +            .name("/a/b/c/d/anotherservice")
    +            .build();
    +    this.serviceRegistry.register(registration);
    +}

    77.1 Instance Status

    Netflix Eureka supports having instances that are OUT_OF_SERVICE registered with the +server. These instances are not returned as active service instances. This is useful for +behaviors such as blue/green deployments. (Note that the Curator Service Discovery recipe +does not support this behavior.) Taking advantage of the flexible payload has let Spring +Cloud Zookeeper implement OUT_OF_SERVICE by updating some specific metadata and then +filtering on that metadata in the Ribbon ZookeeperServerList. The ZookeeperServerList +filters out all non-null instance statuses that do not equal UP. If the instance status +field is empty, it is considered to be UP for backwards compatibility. To change the +status of an instance, make a POST with OUT_OF_SERVICE to the ServiceRegistry +instance status actuator endpoint, as shown in the following example:

    $ http POST http://localhost:8081/service-registry status=OUT_OF_SERVICE
    [Note]Note

    The preceding example uses the http command from https://httpie.org.

    78. Zookeeper Dependencies

    The following topics cover how to work with Spring Cloud Zookeeper dependencies:

    78.1 Using the Zookeeper Dependencies

    Spring Cloud Zookeeper gives you a possibility to provide dependencies of your application +as properties. As dependencies, you can understand other applications that are registered +in Zookeeper and which you would like to call through +Feign +(a REST client builder) and Spring RestTemplate.

    You can also use the Zookeeper Dependency Watchers functionality to control and monitor +the state of your dependencies.

    78.2 Activating Zookeeper Dependencies

    Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-discovery enables +autoconfiguration that sets up Spring Cloud Zookeeper Dependencies. Even if you provide +the dependencies in your properties, you can turn off the dependencies. To do so, set the +spring.cloud.zookeeper.dependency.enabled property to false (it defaults to true).

    78.3 Setting up Zookeeper Dependencies

    Consider the following example of dependency representation:

    application.yml.  +

    spring.application.name: yourServiceName
    +spring.cloud.zookeeper:
    +  dependencies:
    +    newsletter:
    +      path: /path/where/newsletter/has/registered/in/zookeeper
    +      loadBalancerType: ROUND_ROBIN
    +      contentTypeTemplate: application/vnd.newsletter.$version+json
    +      version: v1
    +      headers:
    +        header1:
    +            - value1
    +        header2:
    +            - value2
    +      required: false
    +      stubs: org.springframework:foo:stubs
    +    mailing:
    +      path: /path/where/mailing/has/registered/in/zookeeper
    +      loadBalancerType: ROUND_ROBIN
    +      contentTypeTemplate: application/vnd.mailing.$version+json
    +      version: v1
    +      required: true

    +

    The next few sections go through each part of the dependency one by one. The root property +name is spring.cloud.zookeeper.dependencies.

    78.3.1 Aliases

    Below the root property you have to represent each dependency as an alias. This is due to +the constraints of Ribbon, which requires that the application ID be placed in the URL. +Consequently, you cannot pass any complex path, suchas /myApp/myRoute/name). The alias +is the name you use instead of the serviceId for DiscoveryClient, Feign, or +RestTemplate.

    In the previous examples, the aliases are newsletter and mailing. The following +example shows Feign usage with a newsletter alias:

    @FeignClient("newsletter")
    +public interface NewsletterService {
    +        @RequestMapping(method = RequestMethod.GET, value = "/newsletter")
    +        String getNewsletters();
    +}

    78.3.2 Path

    The path is represented by the path YAML property and is the path under which the +dependency is registered under Zookeeper. As described in the +previous section, Ribbon +operates on URLs. As a result, this path is not compliant with its requirement. +That is why Spring Cloud Zookeeper maps the alias to the proper path.

    78.3.3 Load Balancer Type

    The load balancer type is represented by loadBalancerType YAML property.

    If you know what kind of load-balancing strategy has to be applied when calling this +particular dependency, you can provide it in the YAML file, and it is automatically +applied. You can choose one of the following load balancing strategies:

    • STICKY: Once chosen, the instance is always called.
    • RANDOM: Picks an instance randomly.
    • ROUND_ROBIN: Iterates over instances over and over again.

    78.3.4 Content-Type Template and Version

    The Content-Type template and version are represented by the contentTypeTemplate and +version YAML properties.

    If you version your API in the Content-Type header, you do not want to add this header +to each of your requests. Also, if you want to call a new version of the API, you do not +want to roam around your code to bump up the API version. That is why you can provide a +contentTypeTemplate with a special $version placeholder. That placeholder will be filled by the value of the +version YAML property. Consider the following example of a contentTypeTemplate:

    application/vnd.newsletter.$version+json

    Further consider the following version:

    v1

    The combination of contentTypeTemplate and version results in the creation of a +Content-Type header for each request, as follows:

    application/vnd.newsletter.v1+json

    78.3.5 Default Headers

    Default headers are represented by the headers map in YAML.

    Sometimes, each call to a dependency requires setting up of some default headers. To not +do that in code, you can set them up in the YAML file, as shown in the following example +headers section:

    headers:
    +    Accept:
    +        - text/html
    +        - application/xhtml+xml
    +    Cache-Control:
    +        - no-cache

    That headers section results in adding the Accept and Cache-Control headers with +appropriate list of values in your HTTP request.

    78.3.6 Required Dependencies

    Required dependencies are represented by required property in YAML.

    If one of your dependencies is required to be up when your application boots, you can set +the required: true property in the YAML file.

    If your application cannot localize the required dependency during boot time, it throws an +exception, and the Spring Context fails to set up. In other words, your application cannot +start if the required dependency is not registered in Zookeeper.

    You can read more about Spring Cloud Zookeeper Presence Checker +later in this document.

    78.3.7 Stubs

    You can provide a colon-separated path to the JAR containing stubs of the dependency, as +shown in the following example:

    stubs: org.springframework:myApp:stubs

    where:

    • org.springframework is the groupId.
    • myApp is the artifactId.
    • stubs is the classifier. (Note that stubs is the default value.)

    Because stubs is the default classifier, the preceding example is equal to the following +example:

    stubs: org.springframework:myApp

    78.4 Configuring Spring Cloud Zookeeper Dependencies

    You can set the following properties to enable or disable parts of Zookeeper Dependencies +functionalities:

    • spring.cloud.zookeeper.dependencies: If you do not set this property, you cannot use +Zookeeper Dependencies.
    • spring.cloud.zookeeper.dependency.ribbon.enabled (enabled by default): Ribbon requires +either explicit global configuration or a particular one for a dependency. By turning on +this property, runtime load balancing strategy resolution is possible, and you can use the +loadBalancerType section of the Zookeeper Dependencies. The configuration that needs +this property has an implementation of LoadBalancerClient that delegates to the +ILoadBalancer presented in the next bullet.
    • spring.cloud.zookeeper.dependency.ribbon.loadbalancer (enabled by default): Thanks to +this property, the custom ILoadBalancer knows that the part of the URI passed to Ribbon +might actually be the alias that has to be resolved to a proper path in Zookeeper. Without +this property, you cannot register applications under nested paths.
    • spring.cloud.zookeeper.dependency.headers.enabled (enabled by default): This property +registers a RibbonClient that automatically appends appropriate headers and content +types with their versions, as presented in the Dependency configuration. Without this +setting, those two parameters do not work.
    • spring.cloud.zookeeper.dependency.resttemplate.enabled (enabled by default): When +enabled, this property modifies the request headers of a @LoadBalanced-annotated +RestTemplate such that it passes headers and content type with the version set in +dependency configuration. Without this setting, those two parameters do not work.

    79. Spring Cloud Zookeeper Dependency Watcher

    The Dependency Watcher mechanism lets you register listeners to your dependencies. The +functionality is, in fact, an implementation of the Observator pattern. When a +dependency changes, its state (to either UP or DOWN), some custom logic can be applied.

    79.1 Activating

    Spring Cloud Zookeeper Dependencies functionality needs to be enabled for you to use the +Dependency Watcher mechanism.

    79.2 Registering a Listener

    To register a listener, you must implement an interface called +org.springframework.cloud.zookeeper.discovery.watcher.DependencyWatcherListener and +register it as a bean. The interface gives you one method:

    void stateChanged(String dependencyName, DependencyState newState);

    If you want to register a listener for a particular dependency, the dependencyName would +be the discriminator for your concrete implementation. newState provides you with +information about whether your dependency has changed to CONNECTED or DISCONNECTED.

    79.3 Using the Presence Checker

    Bound with the Dependency Watcher is the functionality called Presence Checker. It lets +you provide custom behavior when your application boots, to react according to the state +of your dependencies.

    The default implementation of the abstract +org.springframework.cloud.zookeeper.discovery.watcher.presence.DependencyPresenceOnStartupVerifier +class is the +org.springframework.cloud.zookeeper.discovery.watcher.presence.DefaultDependencyPresenceOnStartupVerifier, +which works in the following way.

    1. If the dependency is marked us required and is not in Zookeeper, when your application +boots, it throws an exception and shuts down.
    2. If the dependency is not required, the +org.springframework.cloud.zookeeper.discovery.watcher.presence.LogMissingDependencyChecker +logs that the dependency is missing at the WARN level.

    Because the DefaultDependencyPresenceOnStartupVerifier is registered only when there is +no bean of type DependencyPresenceOnStartupVerifier, this functionality can be +overridden.

    80. Distributed Configuration with Zookeeper

    Zookeeper provides a +hierarchical namespace +that lets clients store arbitrary data, such as configuration data. Spring Cloud Zookeeper +Config is an alternative to the +Config Server and Client. +Configuration is loaded into the Spring Environment during the special bootstrap +phase. Configuration is stored in the /config namespace by default. Multiple +PropertySource instances are created, based on the application’s name and the active +profiles, to mimic the Spring Cloud Config order of resolving properties. For example, an +application with a name of testApp and with the dev profile has the following property +sources created for it:

    • config/testApp,dev
    • config/testApp
    • config/application,dev
    • config/application

    The most specific property source is at the top, with the least specific at the bottom. +Properties in the config/application namespace apply to all applications that use +zookeeper for configuration. Properties in the config/testApp namespace are available +only to the instances of the service named testApp.

    Configuration is currently read on startup of the application. Sending a HTTP POST +request to /refresh causes the configuration to be reloaded. Watching the configuration +namespace (which Zookeeper supports) is not currently implemented.

    80.1 Activating

    Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-config enables +autoconfiguration that sets up Spring Cloud Zookeeper Config.

    [Caution]Caution

    When working with version 3.4 of Zookeeper you need to change +the way you include the dependency as described here.

    80.2 Customizing

    Zookeeper Config may be customized by setting the following properties:

    bootstrap.yml.  +

    spring:
    +  cloud:
    +    zookeeper:
    +      config:
    +        enabled: true
    +        root: configuration
    +        defaultContext: apps
    +        profileSeparator: '::'

    +

    • enabled: Setting this value to false disables Zookeeper Config.
    • root: Sets the base namespace for configuration values.
    • defaultContext: Sets the name used by all applications.
    • profileSeparator: Sets the value of the separator used to separate the profile name in +property sources with profiles.

    80.3 Access Control Lists (ACLs)

    You can add authentication information for Zookeeper ACLs by calling the addAuthInfo +method of a CuratorFramework bean. One way to accomplish this is to provide your own +CuratorFramework bean, as shown in the following example:

    @BoostrapConfiguration
    +public class CustomCuratorFrameworkConfig {
    +
    +  @Bean
    +  public CuratorFramework curatorFramework() {
    +    CuratorFramework curator = new CuratorFramework();
    +    curator.addAuthInfo("digest", "user:password".getBytes());
    +    return curator;
    +  }
    +
    +}

    Consult +the ZookeeperAutoConfiguration class +to see how the CuratorFramework bean’s default configuration.

    Alternatively, you can add your credentials from a class that depends on the existing +CuratorFramework bean, as shown in the following example:

    @BoostrapConfiguration
    +public class DefaultCuratorFrameworkConfig {
    +
    +  public ZookeeperConfig(CuratorFramework curator) {
    +    curator.addAuthInfo("digest", "user:password".getBytes());
    +  }
    +
    +}

    The creation of this bean must occur during the boostrapping phase. You can register +configuration classes to run during this phase by annotating them with +@BootstrapConfiguration and including them in a comma-separated list that you set as the +value of the org.springframework.cloud.bootstrap.BootstrapConfiguration property in the +resources/META-INF/spring.factories file, as shown in the following example:

    resources/META-INF/spring.factories.  +

    org.springframework.cloud.bootstrap.BootstrapConfiguration=\
    +my.project.CustomCuratorFrameworkConfig,\
    +my.project.DefaultCuratorFrameworkConfig

    +

    Part XI. Spring Cloud Security

    Spring Cloud Security offers a set of primitives for building secure +applications and services with minimum fuss. A declarative model which +can be heavily configured externally (or centrally) lends itself to +the implementation of large systems of co-operating, remote components, +usually with a central indentity management service. It is also extremely +easy to use in a service platform like Cloud Foundry. Building on +Spring Boot and Spring Security OAuth2 we can quickly create systems that +implement common patterns like single sign on, token relay and token +exchange.

    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github.

    81. Quickstart

    81.1 OAuth2 Single Sign On

    Here’s a Spring Cloud "Hello World" app with HTTP Basic +authentication and a single user account:

    app.groovy.  +

    @Grab('spring-boot-starter-security')
    +@Controller
    +class Application {
    +
    +  @RequestMapping('/')
    +  String home() {
    +    'Hello World'
    +  }
    +
    +}

    +

    You can run it with spring run app.groovy and watch the logs for the password (username is "user"). So far this is just the default for a Spring Boot app.

    Here’s a Spring Cloud app with OAuth2 SSO:

    app.groovy.  +

    @Controller
    +@EnableOAuth2Sso
    +class Application {
    +
    +  @RequestMapping('/')
    +  String home() {
    +    'Hello World'
    +  }
    +
    +}

    +

    Spot the difference? This app will actually behave exactly the same as +the previous one, because it doesn’t know it’s OAuth2 credentals +yet.

    You can register an app in github quite easily, so try that if you +want a production app on your own domain. If you are happy to test on +localhost:8080, then set up these properties in your application +configuration:

    application.yml.  +

    security:
    +  oauth2:
    +    client:
    +      clientId: bd1c0a783ccdd1c9b9e4
    +      clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    +      accessTokenUri: https://github.com/login/oauth/access_token
    +      userAuthorizationUri: https://github.com/login/oauth/authorize
    +      clientAuthenticationScheme: form
    +    resource:
    +      userInfoUri: https://api.github.com/user
    +      preferTokenInfo: false

    +

    run the app above and it will redirect to github for authorization. If +you are already signed into github you won’t even notice that it has +authenticated. These credentials will only work if your app is +running on port 8080.

    To limit the scope that the client asks for when it obtains an access token +you can set security.oauth2.client.scope (comma separated or an array in YAML). By +default the scope is empty and it is up to to Authorization Server to +decide what the defaults should be, usually depending on the settings in +the client registration that it holds.

    [Note]Note

    The examples above are all Groovy scripts. If you want to write the +same code in Java (or Groovy) you need to add Spring Security OAuth2 +to the classpath (e.g. see the +sample here).

    81.2 OAuth2 Protected Resource

    You want to protect an API resource with an OAuth2 token? Here’s a +simple example (paired with the client above):

    app.groovy.  +

    @Grab('spring-cloud-starter-security')
    +@RestController
    +@EnableResourceServer
    +class Application {
    +
    +  @RequestMapping('/')
    +  def home() {
    +    [message: 'Hello World']
    +  }
    +
    +}

    +

    and

    application.yml.  +

    security:
    +  oauth2:
    +    resource:
    +      userInfoUri: https://api.github.com/user
    +      preferTokenInfo: false

    +

    82. More Detail

    82.1 Single Sign On

    [Note]Note

    All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +Spring Boot user guide.

    82.2 Token Relay

    A Token Relay is where an OAuth2 consumer acts as a Client and +forwards the incoming token to outgoing resource requests. The +consumer can be a pure Client (like an SSO application) or a Resource +Server.

    82.2.1 Client Token Relay in Spring Cloud Gateway

    If your app also has a +Spring +Cloud Gateway embedded reverse proxy then you +can ask it to forward OAuth2 access tokens downstream to the services +it is proxying. Thus the SSO app above can be enhanced simply like +this:

    App.java.  +

    @Autowired
    +private TokenRelayGatewayFilterFactory filterFactory;
    +
    +@Bean
    +public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    +    return builder.routes()
    +            .route("resource", r -> r.path("/resource")
    +                    .filters(f -> f.filter(filterFactory.apply()))
    +                    .uri("http://localhost:9000"))
    +            .build();
    +}

    +

    or this

    application.yaml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: resource
    +        uri: http://localhost:9000
    +        predicates:
    +        - Path=/resource
    +        filters:
    +        - TokenRelay=

    +

    and it will (in addition to logging the user in and grabbing a token) +pass the authentication token downstream to the services (in this case +/resource).

    To enable this for Spring Cloud Gateway add the following dependencies

    • org.springframework.boot:spring-boot-starter-oauth2-client
    • org.springframework.cloud:spring-cloud-starter-security

    How does it work? The +filter +extracts an access token from the currently authenticated user, +and puts it in a request header for the downstream requests.

    For a full working sample see this project.

    [Note]Note

    The default implementation of ReactiveOAuth2AuthorizedClientService used by TokenRelayGatewayFilterFactory +uses an in-memory data store. You will need to provide your own implementation ReactiveOAuth2AuthorizedClientService +if you need a more robust solution.

    82.2.2 Client Token Relay

    If your app is a user facing OAuth2 client (i.e. has declared +@EnableOAuth2Sso or @EnableOAuth2Client) then it has an +OAuth2ClientContext in request scope from Spring Boot. You can +create your own OAuth2RestTemplate from this context and an +autowired OAuth2ProtectedResourceDetails, and then the context will +always forward the access token downstream, also refreshing the access +token automatically if it expires. (These are features of Spring +Security and Spring Boot.)

    [Note]Note

    Spring Boot (1.4.1) does not create an +OAuth2ProtectedResourceDetails automatically if you are using +client_credentials tokens. In that case you need to create your own +ClientCredentialsResourceDetails and configure it with +@ConfigurationProperties("security.oauth2.client").

    82.2.3 Client Token Relay in Zuul Proxy

    If your app also has a +Spring +Cloud Zuul embedded reverse proxy (using @EnableZuulProxy) then you +can ask it to forward OAuth2 access tokens downstream to the services +it is proxying. Thus the SSO app above can be enhanced simply like +this:

    app.groovy.  +

    @Controller
    +@EnableOAuth2Sso
    +@EnableZuulProxy
    +class Application {
    +
    +}

    +

    and it will (in addition to logging the user in and grabbing a token) +pass the authentication token downstream to the /proxy/* +services. If those services are implemented with +@EnableResourceServer then they will get a valid token in the +correct header.

    How does it work? The @EnableOAuth2Sso annotation pulls in +spring-cloud-starter-security (which you could do manually in a +traditional app), and that in turn triggers some autoconfiguration for +a ZuulFilter, which itself is activated because Zuul is on the +classpath (via @EnableZuulProxy). The +filter +just extracts an access token from the currently authenticated user, +and puts it in a request header for the downstream requests.

    [Note]Note

    Spring Boot does not create an OAuth2RestOperations automatically which is needed for refresh_token. In that case you need to create your own +OAuth2RestOperations so OAuth2TokenRelayFilter can refresh the token if needed.

    82.2.4 Resource Server Token Relay

    If your app has @EnableResourceServer you might want to relay the +incoming token downstream to other services. If you use a +RestTemplate to contact the downstream services then this is just a +matter of how to create the template with the right context.

    If your service uses UserInfoTokenServices to authenticate incoming +tokens (i.e. it is using the security.oauth2.user-info-uri +configuration), then you can simply create an OAuth2RestTemplate +using an autowired OAuth2ClientContext (it will be populated by the +authentication process before it hits the backend code). Equivalently +(with Spring Boot 1.4), you could inject a +UserInfoRestTemplateFactory and grab its OAuth2RestTemplate in +your configuration. For example:

    MyConfiguration.java.  +

    @Bean
    +public OAuth2RestTemplate restTemplate(UserInfoRestTemplateFactory factory) {
    +    return factory.getUserInfoRestTemplate();
    +}

    +

    This rest template will then have the same OAuth2ClientContext +(request-scoped) that is used by the authentication filter, so you can +use it to send requests with the same access token.

    If your app is not using UserInfoTokenServices but is still a client +(i.e. it declares @EnableOAuth2Client or @EnableOAuth2Sso), then +with Spring Security Cloud any OAuth2RestOperations that the user +creates from an @Autowired OAuth2Context will also forward +tokens. This feature is implemented by default as an MVC handler +interceptor, so it only works in Spring MVC. If you are not using MVC +you could use a custom filter or AOP interceptor wrapping an +AccessTokenContextRelay to provide the same feature.

    Here’s a basic +example showing the use of an autowired rest template created +elsewhere ("foo.com" is a Resource Server accepting the same tokens as +the surrounding app):

    MyController.java.  +

    @Autowired
    +private OAuth2RestOperations restTemplate;
    +
    +@RequestMapping("/relay")
    +public String relay() {
    +    ResponseEntity<String> response =
    +      restTemplate.getForEntity("https://foo.com/bar", String.class);
    +    return "Success! (" + response.getBody() + ")";
    +}

    +

    If you don’t want to forward tokens (and that is a valid +choice, since you might want to act as yourself, rather than the +client that sent you the token), then you only need to create your own +OAuth2Context instead of autowiring the default one.

    Feign clients will also pick up an interceptor that uses the +OAuth2ClientContext if it is available, so they should also do a +token relay anywhere where a RestTemplate would.

    83. Configuring Authentication Downstream of a Zuul Proxy

    You can control the authorization behaviour downstream of an +@EnableZuulProxy through the proxy.auth.* settings. Example:

    application.yml.  +

    proxy:
    +  auth:
    +    routes:
    +      customers: oauth2
    +      stores: passthru
    +      recommendations: none

    +

    In this example the "customers" service gets an OAuth2 token relay, +the "stores" service gets a passthrough (the authorization header is +just passed downstream), and the "recommendations" service has its +authorization header removed. The default behaviour is to do a token +relay if there is a token available, and passthru otherwise.

    See + +ProxyAuthenticationProperties for full details.

    Part XII. Spring Cloud for Cloud Foundry

    Spring Cloud for Cloudfoundry makes it easy to run +Spring Cloud apps in +Cloud Foundry (the Platform as a +Service). Cloud Foundry has the notion of a "service", which is +middlware that you "bind" to an app, essentially providing it with an +environment variable containing credentials (e.g. the location and +username to use for the service).

    The spring-cloud-cloudfoundry-commons module configures the +Reactor-based Cloud Foundry Java client, v 3.0, and can be used standalone.

    The spring-cloud-cloudfoundry-web project provides basic support for +some enhanced features of webapps in Cloud Foundry: binding +automatically to single-sign-on services and optionally enabling +sticky routing for discovery.

    The spring-cloud-cloudfoundry-discovery project provides an +implementation of Spring Cloud Commons DiscoveryClient so you can +@EnableDiscoveryClient and provide your credentials as +spring.cloud.cloudfoundry.discovery.[username,password] (also *.url if you are not connecting to Pivotal Web Services) and then you +can use the DiscoveryClient directly or via a LoadBalancerClient.

    The first time you use it the discovery client might be slow owing to +the fact that it has to get an access token from Cloud Foundry.

    84. Discovery

    Here’s a Spring Cloud app with Cloud Foundry discovery:

    app.groovy.  +

    @Grab('org.springframework.cloud:spring-cloud-cloudfoundry')
    +@RestController
    +@EnableDiscoveryClient
    +class Application {
    +
    +  @Autowired
    +  DiscoveryClient client
    +
    +  @RequestMapping('/')
    +  String home() {
    +    'Hello from ' + client.getLocalServiceInstance()
    +  }
    +
    +}

    +

    If you run it without any service bindings:

    $ spring jar app.jar app.groovy
    +$ cf push -p app.jar

    It will show its app name in the home page.

    The DiscoveryClient can lists all the apps in a space, according to +the credentials it is authenticated with, where the space defaults to +the one the client is running in (if any). If neither org nor space +are configured, they default per the user’s profile in Cloud Foundry.

    85. Single Sign On

    [Note]Note

    All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +Spring Boot user guide.

    This project provides automatic binding from CloudFoundry service +credentials to the Spring Boot features. If you have a CloudFoundry +service called "sso", for instance, with credentials containing +"client_id", "client_secret" and "auth_domain", it will bind +automatically to the Spring OAuth2 client that you enable with +@EnableOAuth2Sso (from Spring Boot). The name of the service can be +parameterized using spring.oauth2.sso.serviceId.

    Part XIII. Spring Cloud Contract

    Documentation Authors: Adam Dudczak, Mathias Düsterhöft, Marcin Grzejszczak, Dennis Kieselhorst, Jakub Kubryński, Karol Lassak, +Olga Maciaszek-Sharma, Mariusz Smykuła, Dave Syer, Jay Bryant

    Greenwich.SR5

    86. Spring Cloud Contract

    You need confidence when pushing new features to a new application or service in a +distributed system. This project provides support for Consumer Driven Contracts and +service schemas in Spring applications (for both HTTP and message-based interactions), +covering a range of options for writing tests, publishing them as assets, and asserting +that a contract is kept by producers and consumers.

    87. Spring Cloud Contract Verifier Introduction

    Spring Cloud Contract Verifier enables Consumer Driven Contract (CDC) development of +JVM-based applications. It moves TDD to the level of software architecture.

    Spring Cloud Contract Verifier ships with Contract Definition Language (CDL). Contract +definitions are used to produce the following resources:

    • JSON stub definitions to be used by WireMock when doing integration testing on the +client code (client tests). Test code must still be written by hand, and test data is +produced by Spring Cloud Contract Verifier.
    • Messaging routes, if you’re using a messaging service. We integrate with Spring +Integration, Spring Cloud Stream, Spring AMQP, and Apache Camel. You can also set your +own integrations.
    • Acceptance tests (in JUnit 4, JUnit 5 or Spock) are used to verify if server-side implementation +of the API is compliant with the contract (server tests). A full test is generated by +Spring Cloud Contract Verifier.

    87.1 History

    Before becoming Spring Cloud Contract, this project was called Accurest. +It was created by Marcin Grzejszczak and Jakub Kubrynski +from (Codearte.

    The 0.1.0 release took place on 26 Jan 2015 and it became stable with 1.0.0 release on 29 Feb 2016.

    87.2 Why a Contract Verifier?

    Assume that we have a system consisting of multiple microservices:

    Microservices Architecture

    87.2.1 Testing issues

    If we wanted to test the application in top left corner to determine whether it can +communicate with other services, we could do one of two things:

    • Deploy all microservices and perform end-to-end tests.
    • Mock other microservices in unit/integration tests.

    Both have their advantages but also a lot of disadvantages.

    Deploy all microservices and perform end to end tests

    Advantages:

    • Simulates production.
    • Tests real communication between services.

    Disadvantages:

    • To test one microservice, we have to deploy 6 microservices, a couple of databases, +etc.
    • The environment where the tests run is locked for a single suite of tests (nobody else +would be able to run the tests in the meantime).
    • They take a long time to run.
    • The feedback comes very late in the process.
    • They are extremely hard to debug.

    Mock other microservices in unit/integration tests

    Advantages:

    • They provide very fast feedback.
    • They have no infrastructure requirements.

    Disadvantages:

    • The implementor of the service creates stubs that might have nothing to do with +reality.
    • You can go to production with passing tests and failing production.

    To solve the aforementioned issues, Spring Cloud Contract Verifier with Stub Runner was +created. The main idea is to give you very fast feedback, without the need to set up the +whole world of microservices. If you work on stubs, then the only applications you need +are those that your application directly uses.

    Stubbed Services

    Spring Cloud Contract Verifier gives you the certainty that the stubs that you use were +created by the service that you’re calling. Also, if you can use them, it means that they +were tested against the producer’s side. In short, you can trust those stubs.

    87.3 Purposes

    The main purposes of Spring Cloud Contract Verifier with Stub Runner are:

    • To ensure that WireMock/Messaging stubs (used when developing the client) do exactly +what the actual server-side implementation does.
    • To promote ATDD method and Microservices architectural style.
    • To provide a way to publish changes in contracts that are immediately visible on both +sides.
    • To generate boilerplate test code to be used on the server side.
    [Important]Important

    Spring Cloud Contract Verifier’s purpose is NOT to start writing business +features in the contracts. Assume that we have a business use case of fraud check. If a +user can be a fraud for 100 different reasons, we would assume that you would create 2 +contracts, one for the positive case and one for the negative case. Contract tests are +used to test contracts between applications and not to simulate full behavior.

    87.4 How It Works

    This section explores how Spring Cloud Contract Verifier with Stub Runner works.

    87.4.1 A Three-second Tour

    This very brief tour walks through using Spring Cloud Contract:

    You can find a somewhat longer tour +here.

    On the Producer Side

    To start working with Spring Cloud Contract, add files with REST/ messaging contracts +expressed in either Groovy DSL or YAML to the contracts directory, which is set by the +contractsDslDir property. By default, it is $rootDir/src/test/resources/contracts.

    Then add the Spring Cloud Contract Verifier dependency and plugin to your build file, as +shown in the following example:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>

    The following listing shows how to add the plugin, which should go in the build/plugins +portion of the file:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +</plugin>

    Running ./mvnw clean install automatically generates tests that verify the application +compliance with the added contracts. By default, the tests get generated under +org.springframework.cloud.contract.verifier.tests..

    As the implementation of the functionalities described by the contracts is not yet +present, the tests fail.

    To make them pass, you must add the correct implementation of either handling HTTP +requests or messages. Also, you must add a correct base test class for auto-generated +tests to the project. This class is extended by all the auto-generated tests, and it +should contain all the setup necessary to run them (for example RestAssuredMockMvc +controller setup or messaging test setup).

    Once the implementation and the test base class are in place, the tests pass, and both the +application and the stub artifacts are built and installed in the local Maven repository. +The changes can now be merged, and both the application and the stub artifacts may be +published in an online repository.

    On the Consumer Side

    Spring Cloud Contract Stub Runner can be used in the integration tests to get a running +WireMock instance or messaging route that simulates the actual service.

    To do so, add the dependency to Spring Cloud Contract Stub Runner, as shown in the +following example:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +	<scope>test</scope>
    +</dependency>

    You can get the Producer-side stubs installed in your Maven repository in either of two +ways:

    • By checking out the Producer side repository and adding contracts and generating the stubs +by running the following commands:

      $ cd local-http-server-repo
      +$ ./mvnw clean install -DskipTests
      [Tip]Tip

      The tests are being skipped because the Producer-side contract implementation is not +in place yet, so the automatically-generated contract tests fail.

    • By getting already-existing producer service stubs from a remote repository. To do so, +pass the stub artifact IDs and artifact repository URL as Spring Cloud Contract +Stub Runner properties, as shown in the following example:

      stubrunner:
      +  ids: 'com.example:http-server-dsl:+:stubs:8080'
      +  repositoryRoot: https://repo.spring.io/libs-snapshot

    Now you can annotate your test class with @AutoConfigureStubRunner. In the annotation, +provide the group-id and artifact-id values for Spring Cloud Contract Stub Runner to +run the collaborators' stubs for you, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment=WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
    +		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {
    [Tip]Tip

    Use the REMOTE stubsMode when downloading stubs from an online repository and +LOCAL for offline work.

    Now, in your integration test, you can receive stubbed versions of HTTP responses or +messages that are expected to be emitted by the collaborator service.

    87.4.2 A Three-minute Tour

    This brief tour walks through using Spring Cloud Contract:

    You can find an even more brief tour +here.

    On the Producer Side

    To start working with Spring Cloud Contract, add files with REST/ messaging contracts +expressed in either Groovy DSL or YAML to the contracts directory, which is set by the +contractsDslDir property. By default, it is $rootDir/src/test/resources/contracts.

    For the HTTP stubs, a contract defines what kind of response should be returned for a +given request (taking into account the HTTP methods, URLs, headers, status codes, and so +on). The following example shows how an HTTP stub contract in Groovy DSL:

    package contracts
    +
    +org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'PUT'
    +		url '/fraudcheck'
    +		body([
    +			   "client.id": $(regex('[0-9]{10}')),
    +			   loanAmount: 99999
    +		])
    +		headers {
    +			contentType('application/json')
    +		}
    +	}
    +	response {
    +		status OK()
    +		body([
    +			   fraudCheckStatus: "FRAUD",
    +			   "rejection.reason": "Amount too high"
    +		])
    +		headers {
    +			contentType('application/json')
    +		}
    +	}
    +}

    The same contract expressed in YAML would look like the following example:

    request:
    +  method: PUT
    +  url: /fraudcheck
    +  body:
    +    "client.id": 1234567890
    +    loanAmount: 99999
    +  headers:
    +    Content-Type: application/json
    +  matchers:
    +    body:
    +      - path: $.['client.id']
    +        type: by_regex
    +        value: "[0-9]{10}"
    +response:
    +  status: 200
    +  body:
    +    fraudCheckStatus: "FRAUD"
    +    "rejection.reason": "Amount too high"
    +  headers:
    +    Content-Type: application/json;charset=UTF-8

    In the case of messaging, you can define:

    • The input and the output messages can be defined (taking into account from and where it +was sent, the message body, and the header).
    • The methods that should be called after the message is received.
    • The methods that, when called, should trigger a message.

    The following example shows a Camel messaging contract expressed in Groovy DSL:

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					messageFrom('jms:delete')
    +					messageBody([
    +							bookName: 'foo'
    +					])
    +					messageHeaders {
    +						header('sample', 'header')
    +					}
    +					assertThat('bookWasDeleted()')
    +				}
    +			}

    The following example shows the same contract expressed in YAML:

    label: some_label
    +input:
    +  messageFrom: jms:delete
    +  messageBody:
    +    bookName: 'foo'
    +  messageHeaders:
    +    sample: header
    +  assertThat: bookWasDeleted()

    Then you can add Spring Cloud Contract Verifier dependency and plugin to your build file, +as shown in the following example:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>

    The following listing shows how to add the plugin, which should go in the build/plugins +portion of the file:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +</plugin>

    Running ./mvnw clean install automatically generates tests that verify the application +compliance with the added contracts. By default, the generated tests are under +org.springframework.cloud.contract.verifier.tests..

    The following example shows a sample auto-generated test for an HTTP contract:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +    // given:
    +        MockMvcRequestSpecification request = given()
    +                .header("Content-Type", "application/vnd.fraud.v1+json")
    +                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
    +
    +    // when:
    +        ResponseOptions response = given().spec(request)
    +                .put("/fraudcheck");
    +
    +    // then:
    +        assertThat(response.statusCode()).isEqualTo(200);
    +        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    +    // and:
    +        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
    +        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
    +}

    The preceding example uses Spring’s MockMvc to run the tests. This is the default test +mode for HTTP contracts. However, JAX-RS client and explicit HTTP invocations can also be +used. (To do so, change the testMode property of the plugin to JAX-RS or EXPLICIT, +respectively.)

    Since 2.1.0, it is also possible to use RestAssuredWebTestClient`with Spring’s reactive `WebTestClient +run under the hood. This is particularly recommended while working with Reactive, Web-Flux-based applications. +In order to use WebTestClient set testMode to WEBTESTCLIENT.

    Here is an example of a test generated in WEBTESTCLIENT test mode:

    [source,java,indent=0]
    @Test
    +	public void validate_shouldRejectABeerIfTooYoung() throws Exception {
    +		// given:
    +			WebTestClientRequestSpecification request = given()
    +					.header("Content-Type", "application/json")
    +					.body("{\"age\":10}");
    +
    +		// when:
    +			WebTestClientResponse response = given().spec(request)
    +					.post("/check");
    +
    +		// then:
    +			assertThat(response.statusCode()).isEqualTo(200);
    +			assertThat(response.header("Content-Type")).matches("application/json.*");
    +		// and:
    +			DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +			assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK");
    +	}

    Apart from the default JUnit 4, you can instead use JUnit 5 or Spock tests, by setting the plugin +testFramework property to either JUNIT5 or Spock.

    [Tip]Tip

    You can now also generate WireMock scenarios based on the contracts, by including an +order number followed by an underscore at the beginning of the contract file names.

    The following example shows an auto-generated test in Spock for a messaging stub contract:

    [source,groovy,indent=0]
    given:
    +	 ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +		\'\'\'{"bookName":"foo"}\'\'\',
    +		['sample': 'header']
    +	)
    +
    +when:
    +	 contractVerifierMessaging.send(inputMessage, 'jms:delete')
    +
    +then:
    +	 noExceptionThrown()
    +	 bookWasDeleted()

    As the implementation of the functionalities described by the contracts is not yet +present, the tests fail.

    To make them pass, you must add the correct implementation of handling either HTTP +requests or messages. Also, you must add a correct base test class for auto-generated +tests to the project. This class is extended by all the auto-generated tests and should +contain all the setup necessary to run them (for example, RestAssuredMockMvc controller +setup or messaging test setup).

    Once the implementation and the test base class are in place, the tests pass, and both the +application and the stub artifacts are built and installed in the local Maven repository. +Information about installing the stubs jar to the local repository appears in the logs, as +shown in the following example:

    [INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
    +[INFO]
    +[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
    +[INFO]
    +[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
    +[INFO]
    +[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
    +[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

    You can now merge the changes and publish both the application and the stub artifacts +in an online repository.

    Docker Project

    In order to enable working with contracts while creating applications in non-JVM +technologies, the springcloud/spring-cloud-contract Docker image has been created. It +contains a project that automatically generates tests for HTTP contracts and executes them +in EXPLICIT test mode. Then, if the tests pass, it generates Wiremock stubs and, +optionally, publishes them to an artifact manager. In order to use the image, you can +mount the contracts into the /contracts directory and set a few environment variables.

    On the Consumer Side

    Spring Cloud Contract Stub Runner can be used in the integration tests to get a running +WireMock instance or messaging route that simulates the actual service.

    To get started, add the dependency to Spring Cloud Contract Stub Runner:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +	<scope>test</scope>
    +</dependency>

    You can get the Producer-side stubs installed in your Maven repository in either of two +ways:

    • By checking out the Producer side repository and adding contracts and generating the +stubs by running the following commands:

      $ cd local-http-server-repo
      +$ ./mvnw clean install -DskipTests
      [Note]Note

      The tests are skipped because the Producer-side contract implementation is not yet +in place, so the automatically-generated contract tests fail.

    • Getting already existing producer service stubs from a remote repository. To do so, +pass the stub artifact IDs and artifact repository URl as Spring Cloud Contract Stub +Runner properties, as shown in the following example:

      stubrunner:
      +  ids: 'com.example:http-server-dsl:+:stubs:8080'
      +  repositoryRoot: https://repo.spring.io/libs-snapshot

    Now you can annotate your test class with @AutoConfigureStubRunner. In the annotation, +provide the group-id and artifact-id for Spring Cloud Contract Stub Runner to run +the collaborators' stubs for you, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment=WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
    +		stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {
    [Tip]Tip

    Use the REMOTE stubsMode when downloading stubs from an online repository and +LOCAL for offline work.

    In your integration test, you can receive stubbed versions of HTTP responses or messages +that are expected to be emitted by the collaborator service. You can see entries similar +to the following in the build logs:

    2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
    +2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
    +2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
    +2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
    +2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
    +2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
    +2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]

    87.4.3 Defining the Contract

    As consumers of services, we need to define what exactly we want to achieve. We need to +formulate our expectations. That is why we write contracts.

    Assume that you want to send a request containing the ID of a client company and the +amount it wants to borrow from us. You also want to send it to the /fraudcheck url via +the PUT method.

    Groovy DSL.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package contracts
    +
    +org.springframework.cloud.contract.spec.Contract.make {
    +	request { // (1)
    +		method 'PUT' // (2)
    +		url '/fraudcheck' // (3)
    +		body([ // (4)
    +			   "client.id": $(regex('[0-9]{10}')),
    +			   loanAmount : 99999
    +		])
    +		headers { // (5)
    +			contentType('application/json')
    +		}
    +	}
    +	response { // (6)
    +		status OK() // (7)
    +		body([ // (8)
    +			   fraudCheckStatus  : "FRAUD",
    +			   "rejection.reason": "Amount too high"
    +		])
    +		headers { // (9)
    +			contentType('application/json')
    +		}
    +	}
    +}
    +
    +/*
    +From the Consumer perspective, when shooting a request in the integration test:
    +
    +(1) - If the consumer sends a request
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the response will be sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` equal to `application/json`
    +
    +From the Producer perspective, in the autogenerated producer-side test:
    +
    +(1) - A request will be sent to the producer
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the test will assert if the response has been sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` matching `application/json.*`
    + */

    +

    YAML.  +

    request: # (1)
    +  method: PUT # (2)
    +  url: /fraudcheck # (3)
    +  body: # (4)
    +    "client.id": 1234567890
    +    loanAmount: 99999
    +  headers: # (5)
    +    Content-Type: application/json
    +  matchers:
    +    body:
    +      - path: $.['client.id'] # (6)
    +        type: by_regex
    +        value: "[0-9]{10}"
    +response: # (7)
    +  status: 200 # (8)
    +  body:  # (9)
    +    fraudCheckStatus: "FRAUD"
    +    "rejection.reason": "Amount too high"
    +  headers: # (10)
    +    Content-Type: application/json;charset=UTF-8
    +
    +
    +#From the Consumer perspective, when shooting a request in the integration test:
    +#
    +#(1) - If the consumer sends a request
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
    +#(7) - then the response will be sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json`
    +#
    +#From the Producer perspective, in the autogenerated producer-side test:
    +#
    +#(1) - A request will be sent to the producer
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id` `1234567890`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(7) - then the test will assert if the response has been sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8`

    +

    87.4.4 Client Side

    Spring Cloud Contract generates stubs, which you can use during client-side testing. +You get a running WireMock instance/Messaging route that simulates the service. +You would like to feed that instance with a proper stub definition.

    At some point in time, you need to send a request to the Fraud Detection service.

    ResponseEntity<FraudServiceResponse> response = restTemplate.exchange(
    +		"http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
    +		new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class);

    Annotate your test class with @AutoConfigureStubRunner. In the annotation provide the group id and artifact id for the Stub Runner to download stubs of your collaborators.

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {
    +		"com.example:http-server-dsl:+:stubs:6565" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {

    After that, during the tests, Spring Cloud Contract automatically finds the stubs +(simulating the real service) in the Maven repository and exposes them on a configured +(or random) port.

    87.4.5 Server Side

    Since you are developing your stub, you need to be sure that it actually resembles your +concrete implementation. You cannot have a situation where your stub acts in one way and +your application behaves in a different way, especially in production.

    To ensure that your application behaves the way you define in your stub, tests are +generated from the stub you provide.

    The autogenerated test looks, more or less, like this:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +    // given:
    +        MockMvcRequestSpecification request = given()
    +                .header("Content-Type", "application/vnd.fraud.v1+json")
    +                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
    +
    +    // when:
    +        ResponseOptions response = given().spec(request)
    +                .put("/fraudcheck");
    +
    +    // then:
    +        assertThat(response.statusCode()).isEqualTo(200);
    +        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    +    // and:
    +        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
    +        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
    +}

    87.5 Step-by-step Guide to Consumer Driven Contracts (CDC)

    Consider an example of Fraud Detection and the Loan Issuance process. The business +scenario is such that we want to issue loans to people but do not want them to steal from +us. The current implementation of our system grants loans to everybody.

    Assume that Loan Issuance is a client to the Fraud Detection server. In the current +sprint, we must develop a new feature: if a client wants to borrow too much money, then +we mark the client as a fraud.

    Technical remark - Fraud Detection has an artifact-id of http-server, while Loan +Issuance has an artifact-id of http-client, and both have a group-id of com.example.

    Social remark - both client and server development teams need to communicate directly and +discuss changes while going through the process. CDC is all about communication.

    The server +side code is available here and the +client code here.

    [Tip]Tip

    In this case, the producer owns the contracts. Physically, all the contract are +in the producer’s repository.

    87.5.1 Technical note

    If using the SNAPSHOT / Milestone / Release Candidate versions please add the +following section to your build:

    Maven.  +

    <repositories>
    +	<repository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +</repositories>
    +<pluginRepositories>
    +	<pluginRepository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +</pluginRepositories>

    +

    Gradle.  +

    repositories {
    +	mavenCentral()
    +	mavenLocal()
    +	maven { url "https://repo.spring.io/snapshot" }
    +	maven { url "https://repo.spring.io/milestone" }
    +	maven { url "https://repo.spring.io/release" }
    +}

    +

    87.5.2 Consumer side (Loan Issuance)

    As a developer of the Loan Issuance service (a consumer of the Fraud Detection server), you might do the following steps:

    1. Start doing TDD by writing a test for your feature.
    2. Write the missing implementation.
    3. Clone the Fraud Detection service repository locally.
    4. Define the contract locally in the repo of Fraud Detection service.
    5. Add the Spring Cloud Contract Verifier plugin.
    6. Run the integration tests.
    7. File a pull request.
    8. Create an initial implementation.
    9. Take over the pull request.
    10. Write the missing implementation.
    11. Deploy your app.
    12. Work online.

    Start doing TDD by writing a test for your feature.

    @Test
    +public void shouldBeRejectedDueToAbnormalLoanAmount() {
    +	// given:
    +	LoanApplication application = new LoanApplication(new Client("1234567890"),
    +			99999);
    +	// when:
    +	LoanApplicationResult loanApplication = service.loanApplication(application);
    +	// then:
    +	assertThat(loanApplication.getLoanApplicationStatus())
    +			.isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED);
    +	assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high");
    +}

    Assume that you have written a test of your new feature. If a loan application for a big +amount is received, the system should reject that loan application with some description.

    Write the missing implementation.

    At some point in time, you need to send a request to the Fraud Detection service. Assume +that you need to send the request containing the ID of the client and the amount the +client wants to borrow. You want to send it to the /fraudcheck url via the PUT method.

    ResponseEntity<FraudServiceResponse> response = restTemplate.exchange(
    +		"http://localhost:" + port + "/fraudcheck", HttpMethod.PUT,
    +		new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class);

    For simplicity, the port of the Fraud Detection service is set to 8080, and the +application runs on 8090.

    If you start the test at this point, it breaks, because no service currently runs on port +8080.

    Clone the Fraud Detection service repository locally.

    You can start by playing around with the server side contract. To do so, you must first +clone it.

    $ git clone https://your-git-server.com/server-side.git local-http-server-repo

    Define the contract locally in the repo of Fraud Detection service.

    As a consumer, you need to define what exactly you want to achieve. You need to formulate +your expectations. To do so, write the following contract:

    [Important]Important

    Place the contract under src/test/resources/contracts/fraud folder. The fraud folder +is important because the producer’s test base class name references that folder.

    Groovy DSL.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package contracts
    +
    +org.springframework.cloud.contract.spec.Contract.make {
    +	request { // (1)
    +		method 'PUT' // (2)
    +		url '/fraudcheck' // (3)
    +		body([ // (4)
    +			   "client.id": $(regex('[0-9]{10}')),
    +			   loanAmount : 99999
    +		])
    +		headers { // (5)
    +			contentType('application/json')
    +		}
    +	}
    +	response { // (6)
    +		status OK() // (7)
    +		body([ // (8)
    +			   fraudCheckStatus  : "FRAUD",
    +			   "rejection.reason": "Amount too high"
    +		])
    +		headers { // (9)
    +			contentType('application/json')
    +		}
    +	}
    +}
    +
    +/*
    +From the Consumer perspective, when shooting a request in the integration test:
    +
    +(1) - If the consumer sends a request
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the response will be sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` equal to `application/json`
    +
    +From the Producer perspective, in the autogenerated producer-side test:
    +
    +(1) - A request will be sent to the producer
    +(2) - With the "PUT" method
    +(3) - to the URL "/fraudcheck"
    +(4) - with the JSON body that
    + * has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
    + * has a field `loanAmount` that is equal to `99999`
    +(5) - with header `Content-Type` equal to `application/json`
    +(6) - then the test will assert if the response has been sent with
    +(7) - status equal `200`
    +(8) - and JSON body equal to
    + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +(9) - with header `Content-Type` matching `application/json.*`
    + */

    +

    YAML.  +

    request: # (1)
    +  method: PUT # (2)
    +  url: /fraudcheck # (3)
    +  body: # (4)
    +    "client.id": 1234567890
    +    loanAmount: 99999
    +  headers: # (5)
    +    Content-Type: application/json
    +  matchers:
    +    body:
    +      - path: $.['client.id'] # (6)
    +        type: by_regex
    +        value: "[0-9]{10}"
    +response: # (7)
    +  status: 200 # (8)
    +  body:  # (9)
    +    fraudCheckStatus: "FRAUD"
    +    "rejection.reason": "Amount too high"
    +  headers: # (10)
    +    Content-Type: application/json;charset=UTF-8
    +
    +
    +#From the Consumer perspective, when shooting a request in the integration test:
    +#
    +#(1) - If the consumer sends a request
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
    +#(7) - then the response will be sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json`
    +#
    +#From the Producer perspective, in the autogenerated producer-side test:
    +#
    +#(1) - A request will be sent to the producer
    +#(2) - With the "PUT" method
    +#(3) - to the URL "/fraudcheck"
    +#(4) - with the JSON body that
    +# * has a field `client.id` `1234567890`
    +# * has a field `loanAmount` that is equal to `99999`
    +#(5) - with header `Content-Type` equal to `application/json`
    +#(7) - then the test will assert if the response has been sent with
    +#(8) - status equal `200`
    +#(9) - and JSON body equal to
    +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
    +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8`

    +

    The YML contract is quite straight-forward. However when you take a look at the Contract +written using a statically typed Groovy DSL - you might wonder what the +value(client(…​), server(…​)) parts are. By using this notation, Spring Cloud +Contract lets you define parts of a JSON block, a URL, etc., which are dynamic. In case +of an identifier or a timestamp, you need not hardcode a value. You want to allow some +different ranges of values. To enable ranges of values, you can set regular expressions +matching those values for the consumer side. You can provide the body by means of either +a map notation or String with interpolations. +Consult the Chapter 94, Contract DSL section for more information. We highly recommend using the map notation!

    [Tip]Tip

    You must understand the map notation in order to set up contracts. Please read the +Groovy docs regarding JSON.

    The previously shown contract is an agreement between two sides that:

    • if an HTTP request is sent with all of

      • a PUT method on the /fraudcheck endpoint,
      • a JSON body with a client.id that matches the regular expression [0-9]{10} and +loanAmount equal to 99999,
      • and a Content-Type header with a value of application/vnd.fraud.v1+json,
    • then an HTTP response is sent to the consumer that

      • has status 200,
      • contains a JSON body with the fraudCheckStatus field containing a value FRAUD and +the rejectionReason field having value Amount too high,
      • and a Content-Type header with a value of application/vnd.fraud.v1+json.

    Once you are ready to check the API in practice in the integration tests, you need to +install the stubs locally.

    Add the Spring Cloud Contract Verifier plugin.

    We can add either a Maven or a Gradle plugin. In this example, you see how to add Maven. +First, add the Spring Cloud Contract BOM.

    <dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>${spring-cloud-release.version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    Next, add the Spring Cloud Contract Verifier Maven plugin

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +		<convertToYaml>true</convertToYaml>
    +	</configuration>
    +</plugin>

    Since the plugin was added, you get the Spring Cloud Contract Verifier features which, +from the provided contracts:

    • generate and run tests
    • produce and install stubs

    You do not want to generate tests since you, as the consumer, want only to play with the +stubs. You need to skip the test generation and execution. When you execute:

    $ cd local-http-server-repo
    +$ ./mvnw clean install -DskipTests

    In the logs, you see something like this:

    [INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
    +[INFO]
    +[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
    +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
    +[INFO]
    +[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
    +[INFO]
    +[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
    +[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
    +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

    The following line is extremely important:

    [INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar

    It confirms that the stubs of the http-server have been installed in the local +repository.

    Run the integration tests.

    In order to profit from the Spring Cloud Contract Stub Runner functionality of automatic +stub downloading, you must do the following in your consumer side project (Loan +Application service):

    Add the Spring Cloud Contract BOM:

    <dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>${spring-cloud-release-train.version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    Add the dependency to Spring Cloud Contract Stub Runner:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +	<scope>test</scope>
    +</dependency>

    Annotate your test class with @AutoConfigureStubRunner. In the annotation, provide the +group-id and artifact-id for the Stub Runner to download the stubs of your +collaborators. (Optional step) Because you’re playing with the collaborators offline, you +can also provide the offline work switch (StubRunnerProperties.StubsMode.LOCAL).

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.NONE)
    +@AutoConfigureStubRunner(ids = {
    +		"com.example:http-server-dsl:+:stubs:6565" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
    +public class LoanApplicationServiceTests {

    Now, when you run your tests, you see something like this:

    2016-07-19 14:22:25.403  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is + - will try to resolve the latest version
    +2016-07-19 14:22:25.438  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is 0.0.1-SNAPSHOT
    +2016-07-19 14:22:25.439  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
    +2016-07-19 14:22:25.451  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
    +2016-07-19 14:22:25.465  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
    +2016-07-19 14:22:25.475  INFO 41050 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
    +2016-07-19 14:22:27.737  INFO 41050 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]

    This output means that Stub Runner has found your stubs and started a server for your app +with group id com.example, artifact id http-server with version 0.0.1-SNAPSHOT of +the stubs and with stubs classifier on port 8080.

    File a pull request.

    What you have done until now is an iterative process. You can play around with the +contract, install it locally, and work on the consumer side until the contract works as +you wish.

    Once you are satisfied with the results and the test passes, publish a pull request to +the server side. Currently, the consumer side work is done.

    87.5.3 Producer side (Fraud Detection server)

    As a developer of the Fraud Detection server (a server to the Loan Issuance service):

    Create an initial implementation.

    As a reminder, you can see the initial implementation here:

    @RequestMapping(value = "/fraudcheck", method = PUT)
    +public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
    +return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
    +}

    Take over the pull request.

    $ git checkout -b contract-change-pr master
    +$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr

    You must add the dependencies needed by the autogenerated tests:

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>

    In the configuration of the Maven plugin, pass the packageWithBaseClasses property

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +		<convertToYaml>true</convertToYaml>
    +	</configuration>
    +</plugin>
    [Important]Important

    This example uses "convention based" naming by setting the +packageWithBaseClasses property. Doing so means that the two last packages combine to +make the name of the base test class. In our case, the contracts were placed under +src/test/resources/contracts/fraud. Since you do not have two packages starting from +the contracts folder, pick only one, which should be fraud. Add the Base suffix and +capitalize fraud. That gives you the FraudBase test class name.

    All the generated tests extend that class. Over there, you can set up your Spring Context +or whatever is necessary. In this case, use Rest Assured MVC to +start the server side FraudDetectionController.

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package com.example.fraud;
    +
    +import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +
    +public class FraudBase {
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
    +				new FraudStatsController(stubbedStatsProvider()));
    +	}
    +
    +	private StatsProvider stubbedStatsProvider() {
    +		return fraudType -> {
    +			switch (fraudType) {
    +			case DRUNKS:
    +				return 100;
    +			case ALL:
    +				return 200;
    +			}
    +			return 0;
    +		};
    +	}
    +
    +	public void assertThatRejectionReasonIsNull(Object rejectionReason) {
    +		assert rejectionReason == null;
    +	}
    +
    +}

    Now, if you run the ./mvnw clean install, you get something like this:

    Results :
    +
    +Tests in error:
    +  ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed...

    This error occurs because you have a new contract from which a test was generated and it +failed since you have not implemented the feature. The auto-generated test would look +like this:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +    // given:
    +        MockMvcRequestSpecification request = given()
    +                .header("Content-Type", "application/vnd.fraud.v1+json")
    +                .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
    +
    +    // when:
    +        ResponseOptions response = given().spec(request)
    +                .put("/fraudcheck");
    +
    +    // then:
    +        assertThat(response.statusCode()).isEqualTo(200);
    +        assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
    +    // and:
    +        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +        assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
    +        assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
    +}

    If you used the Groovy DSL, you can see, all the producer() parts of the Contract that were present in the +value(consumer(…​), producer(…​)) blocks got injected into the test. +In case of using YAML, the same applied for the matchers sections of the response.

    Note that, on the producer side, you are also doing TDD. The expectations are expressed +in the form of a test. This test sends a request to our own application with the URL, +headers, and body defined in the contract. It also is expecting precisely defined values +in the response. In other words, you have the red part of red, green, and +refactor. It is time to convert the red into the green.

    Write the missing implementation.

    Because you know the expected input and expected output, you can write the missing +implementation:

    @RequestMapping(value = "/fraudcheck", method = PUT)
    +public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) {
    +if (amountGreaterThanThreshold(fraudCheck)) {
    +	return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH);
    +}
    +return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON);
    +}

    When you execute ./mvnw clean install again, the tests pass. Since the Spring Cloud +Contract Verifier plugin adds the tests to the generated-test-sources, you can +actually run those tests from your IDE.

    Deploy your app.

    Once you finish your work, you can deploy your change. First, merge the branch:

    $ git checkout master
    +$ git merge --no-ff contract-change-pr
    +$ git push origin master

    Your CI might run something like ./mvnw clean deploy, which would publish both the +application and the stub artifacts.

    87.5.4 Consumer Side (Loan Issuance) Final Step

    As a developer of the Loan Issuance service (a consumer of the Fraud Detection server):

    Merge branch to master.

    $ git checkout master
    +$ git merge --no-ff contract-change-pr

    Work online.

    Now you can disable the offline work for Spring Cloud Contract Stub Runner and indicate +where the repository with your stubs is located. At this moment the stubs of the server +side are automatically downloaded from Nexus/Artifactory. You can set the value of +stubsMode to REMOTE. The following code shows an example of +achieving the same thing by changing the properties.

    stubrunner:
    +  ids: 'com.example:http-server-dsl:+:stubs:8080'
    +  repositoryRoot: https://repo.spring.io/libs-snapshot

    That’s it!

    87.6 Dependencies

    The best way to add dependencies is to use the proper starter dependency.

    For stub-runner, use spring-cloud-starter-stub-runner. When you use a plugin, add +spring-cloud-starter-contract-verifier.

    87.7 Additional Links

    Here are some resources related to Spring Cloud Contract Verifier and Stub Runner. Note +that some may be outdated, because the Spring Cloud Contract Verifier project is under +constant development.

    87.7.1 Spring Cloud Contract video

    You can check out the video from the Warsaw JUG about Spring Cloud Contract:

    87.8 Samples

    You can find some samples at +samples.

    88. Spring Cloud Contract FAQ

    88.1 Why use Spring Cloud Contract Verifier and not X ?

    For the time being Spring Cloud Contract is a JVM based tool. So it could be your first pick when you’re already creating +software for the JVM. This project has a lot of really interesting features but especially quite a few of them definitely make +Spring Cloud Contract Verifier stand out on the "market" of Consumer Driven Contract (CDC) tooling. Out of many the most interesting are:

    • Possibility to do CDC with messaging
    • Clear and easy to use, statically typed DSL
    • Possibility to copy paste your current JSON file to the contract and only edit its elements
    • Automatic generation of tests from the defined Contract
    • Stub Runner functionality - the stubs are automatically downloaded at runtime from Nexus / Artifactory
    • Spring Cloud integration - no discovery service is needed for integration tests
    • Spring Cloud Contract integrates with Pact out of the box and provides easy hooks to extend its functionality
    • Via Docker adds support for any language & framework used

    88.2 I don’t want to write a contract in Groovy!

    No problem. You can write a contract in YAML!

    88.3 What is this value(consumer(), producer()) ?

    One of the biggest challenges related to stubs is their reusability. Only if they can be vastly used, will they serve their purpose. +What typically makes that difficult are the hard-coded values of request / response elements. For example dates or ids. +Imagine the following JSON request

    {
    +    "time" : "2016-10-10 20:10:15",
    +    "id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
    +    "body" : "foo"
    +}

    and JSON response

    {
    +    "time" : "2016-10-10 21:10:15",
    +    "id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
    +    "body" : "bar"
    +}

    Imagine the pain required to set proper value of the time field (let’s assume that this content is generated by the +database) by changing the clock in the system or providing stub implementations of data providers. The same is related +to the field called id. Will you create a stubbed implementation of UUID generator? Makes little sense…​

    So as a consumer you would like to send a request that matches any form of a time or any UUID. That way your system +will work as usual - will generate data and you won’t have to stub anything out. Let’s assume that in case of the aforementioned +JSON the most important part is the body field. You can focus on that and provide matching for other fields. In other words +you would like the stub to work like this:

    {
    +    "time" : "SOMETHING THAT MATCHES TIME",
    +    "id" : "SOMETHING THAT MATCHES UUID",
    +    "body" : "foo"
    +}

    As far as the response goes as a consumer you need a concrete value that you can operate on. So such a JSON is valid

    {
    +    "time" : "2016-10-10 21:10:15",
    +    "id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051",
    +    "body" : "bar"
    +}

    As you could see in the previous sections we generate tests from contracts. So from the producer’s side the situation looks +much different. We’re parsing the provided contract and in the test we want to send a real request to your endpoints. +So for the case of a producer for the request we can’t have any sort of matching. We need concrete values that the +producer’s backend can work on. Such a JSON would be a valid one:

    {
    +    "time" : "2016-10-10 20:10:15",
    +    "id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
    +    "body" : "foo"
    +}

    On the other hand from the point of view of the validity of the contract the response doesn’t necessarily have to +contain concrete values of time or id. Let’s say that you generate those on the producer side - again, you’d +have to do a lot of stubbing to ensure that you always return the same values. That’s why from the producer’s side +what you might want is the following response:

    {
    +    "time" : "SOMETHING THAT MATCHES TIME",
    +    "id" : "SOMETHING THAT MATCHES UUID",
    +    "body" : "bar"
    +}

    How can you then provide one time a matcher for the consumer and a concrete value for the producer and vice versa? +In Spring Cloud Contract we’re allowing you to provide a dynamic value. That means that it can differ for both +sides of the communication. You can pass the values:

    Either via the value method

    value(consumer(...), producer(...))
    +value(stub(...), test(...))
    +value(client(...), server(...))

    or using the $() method

    $(consumer(...), producer(...))
    +$(stub(...), test(...))
    +$(client(...), server(...))

    You can read more about this in the Chapter 94, Contract DSL section.

    Calling value() or $() tells Spring Cloud Contract that you will be passing a dynamic value. +Inside the consumer() method you pass the value that should be used on the consumer side (in the generated stub). +Inside the producer() method you pass the value that should be used on the producer side (in the generated test).

    [Tip]Tip

    If on one side you have passed the regular expression and you haven’t passed the other, then the +other side will get auto-generated.

    Most often you will use that method together with the regex helper method. E.g. consumer(regex('[0-9]{10}')).

    To sum it up the contract for the aforementioned scenario would look more or less like this (the regular expression +for time and UUID are simplified and most likely invalid but we want to keep things very simple in this example):

    org.springframework.cloud.contract.spec.Contract.make {
    +				request {
    +					method 'GET'
    +					url '/someUrl'
    +					body([
    +					    time : value(consumer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
    +					    id: value(consumer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
    +					    body: "foo"
    +					])
    +				}
    +			response {
    +				status OK()
    +				body([
    +					    time : value(producer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
    +					    id: value([producer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
    +					    body: "bar"
    +					])
    +			}
    +}
    [Important]Important

    Please read the Groovy docs related to JSON to understand how to +properly structure the request / response bodies.

    88.4 How to do Stubs versioning?

    88.4.1 API Versioning

    Let’s try to answer a question what versioning really means. If you’re referring to the API version then there are +different approaches.

    • use Hypermedia, links and do not version your API by any means
    • pass versions through headers / urls

    I will not try to answer a question which approach is better. Whatever suits your needs and allows you to generate +business value should be picked.

    Let’s assume that you do version your API. In that case you should provide as many contracts as many versions you support. +You can create a subfolder for every version or append it to the contract name - whatever suits you more.

    88.4.2 JAR versioning

    If by versioning you mean the version of the JAR that contains the stubs then there are essentially two main approaches.

    Let’s assume that you’re doing Continuous Delivery / Deployment which means that you’re generating a new version of +the jar each time you go through the pipeline and that jar can go to production at any time. For example your jar version +looks like this (it got built on the 20.10.2016 at 20:15:21) :

    1.0.0.20161020-201521-RELEASE

    In that case your generated stub jar will look like this.

    1.0.0.20161020-201521-RELEASE-stubs.jar

    In this case you should inside your application.yml or @AutoConfigureStubRunner when referencing stubs provide the + latest version of the stubs. You can do that by passing the + sign. Example

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})

    If the versioning however is fixed (e.g. 1.0.4.RELEASE or 2.1.1) then you have to set the concrete value of the jar +version. Example for 2.1.1.

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:2.1.1:stubs:8080"})

    88.4.3 Dev or prod stubs

    You can manipulate the classifier to run the tests against current development version of the stubs of other services + or the ones that were deployed to production. If you alter your build to deploy the stubs with the prod-stubs classifier + once you reach production deployment then you can run tests in one case with dev stubs and one with prod stubs.

    Example of tests using development version of stubs

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"})

    Example of tests using production version of stubs

    @AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:prod-stubs:8080"})

    You can pass those values also via properties from your deployment pipeline.

    88.5 Common repo with contracts

    Another way of storing contracts other than having them with the producer is keeping them in a common place. +It can be related to security issues where the consumers can’t clone the producer’s code. Also if you keep +contracts in a single place then you, as a producer, will know how many consumers you have and which +consumer you will break with your local changes.

    88.5.1 Repo structure

    Let’s assume that we have a producer with coordinates com.example:server and 3 consumers: client1, +client2, client3. Then in the repository with common contracts you would have the following setup +(which you can checkout here):

    ├── com
    +│   └── example
    +│       └── server
    +│           ├── client1
    +│           │   └── expectation.groovy
    +│           ├── client2
    +│           │   └── expectation.groovy
    +│           ├── client3
    +│           │   └── expectation.groovy
    +│           └── pom.xml
    +├── mvnw
    +├── mvnw.cmd
    +├── pom.xml
    +└── src
    +    └── assembly
    +        └── contracts.xml

    As you can see under the slash-delimited groupid / artifact id folder (com/example/server) you have +expectations of the 3 consumers (client1, client2 and client3). Expectations are the standard Groovy DSL +contract files as described throughout this documentation. This repository has to produce a JAR file that maps +one to one to the contents of the repo.

    Example of a pom.xml inside the server folder.

    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +		 xmlns="http://maven.apache.org/POM/4.0.0"
    +		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    +	<modelVersion>4.0.0</modelVersion>
    +
    +	<groupId>com.example</groupId>
    +	<artifactId>server</artifactId>
    +	<version>0.0.1-SNAPSHOT</version>
    +
    +	<name>Server Stubs</name>
    +	<description>POM used to install locally stubs for consumer side</description>
    +
    +	<parent>
    +		<groupId>org.springframework.boot</groupId>
    +		<artifactId>spring-boot-starter-parent</artifactId>
    +		<version>2.1.10.RELEASE</version>
    +		<relativePath/>
    +	</parent>
    +
    +	<properties>
    +		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +		<java.version>1.8</java.version>
    +		<spring-cloud-contract.version>2.1.6.BUILD-SNAPSHOT</spring-cloud-contract.version>
    +		<spring-cloud-release.version>Greenwich.BUILD-SNAPSHOT
    +		</spring-cloud-release.version>
    +		<excludeBuildFolders>true</excludeBuildFolders>
    +	</properties>
    +
    +	<dependencyManagement>
    +		<dependencies>
    +			<dependency>
    +				<groupId>org.springframework.cloud</groupId>
    +				<artifactId>spring-cloud-dependencies</artifactId>
    +				<version>${spring-cloud-release.version}</version>
    +				<type>pom</type>
    +				<scope>import</scope>
    +			</dependency>
    +		</dependencies>
    +	</dependencyManagement>
    +
    +	<build>
    +		<plugins>
    +			<plugin>
    +				<groupId>org.springframework.cloud</groupId>
    +				<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +				<version>${spring-cloud-contract.version}</version>
    +				<extensions>true</extensions>
    +				<configuration>
    +					<!-- By default it would search under src/test/resources/ -->
    +					<contractsDirectory>${project.basedir}</contractsDirectory>
    +				</configuration>
    +			</plugin>
    +		</plugins>
    +	</build>
    +
    +	<repositories>
    +		<repository>
    +			<id>spring-snapshots</id>
    +			<name>Spring Snapshots</name>
    +			<url>https://repo.spring.io/snapshot</url>
    +			<snapshots>
    +				<enabled>true</enabled>
    +			</snapshots>
    +		</repository>
    +		<repository>
    +			<id>spring-milestones</id>
    +			<name>Spring Milestones</name>
    +			<url>https://repo.spring.io/milestone</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</repository>
    +		<repository>
    +			<id>spring-releases</id>
    +			<name>Spring Releases</name>
    +			<url>https://repo.spring.io/release</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</repository>
    +	</repositories>
    +	<pluginRepositories>
    +		<pluginRepository>
    +			<id>spring-snapshots</id>
    +			<name>Spring Snapshots</name>
    +			<url>https://repo.spring.io/snapshot</url>
    +			<snapshots>
    +				<enabled>true</enabled>
    +			</snapshots>
    +		</pluginRepository>
    +		<pluginRepository>
    +			<id>spring-milestones</id>
    +			<name>Spring Milestones</name>
    +			<url>https://repo.spring.io/milestone</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</pluginRepository>
    +		<pluginRepository>
    +			<id>spring-releases</id>
    +			<name>Spring Releases</name>
    +			<url>https://repo.spring.io/release</url>
    +			<snapshots>
    +				<enabled>false</enabled>
    +			</snapshots>
    +		</pluginRepository>
    +	</pluginRepositories>
    +
    +</project>

    As you can see there are no dependencies other than the Spring Cloud Contract Maven Plugin. +Those poms are necessary for the consumer side to run mvn clean install -DskipTests to locally install + stubs of the producer project.

    The pom.xml in the root folder can look like this:

    <?xml version="1.0" encoding="UTF-8"?>
    +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +		 xmlns="http://maven.apache.org/POM/4.0.0"
    +		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    +	<modelVersion>4.0.0</modelVersion>
    +
    +	<groupId>com.example.standalone</groupId>
    +	<artifactId>contracts</artifactId>
    +	<version>0.0.1-SNAPSHOT</version>
    +
    +	<name>Contracts</name>
    +	<description>Contains all the Spring Cloud Contracts, well, contracts. JAR used by the
    +		producers to generate tests and stubs
    +	</description>
    +
    +	<properties>
    +		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +	</properties>
    +
    +	<build>
    +		<plugins>
    +			<plugin>
    +				<groupId>org.apache.maven.plugins</groupId>
    +				<artifactId>maven-assembly-plugin</artifactId>
    +				<executions>
    +					<execution>
    +						<id>contracts</id>
    +						<phase>prepare-package</phase>
    +						<goals>
    +							<goal>single</goal>
    +						</goals>
    +						<configuration>
    +							<attach>true</attach>
    +							<descriptor>${basedir}/src/assembly/contracts.xml</descriptor>
    +							<!-- If you want an explicit classifier remove the following line -->
    +							<appendAssemblyId>false</appendAssemblyId>
    +						</configuration>
    +					</execution>
    +				</executions>
    +			</plugin>
    +		</plugins>
    +	</build>
    +
    +</project>

    It’s using the assembly plugin in order to build the JAR with all the contracts. Example of such setup is here:

    <assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +		  xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    +		  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
    +	<id>project</id>
    +	<formats>
    +		<format>jar</format>
    +	</formats>
    +	<includeBaseDirectory>false</includeBaseDirectory>
    +	<fileSets>
    +		<fileSet>
    +			<directory>${project.basedir}</directory>
    +			<outputDirectory>/</outputDirectory>
    +			<useDefaultExcludes>true</useDefaultExcludes>
    +			<excludes>
    +				<exclude>**/${project.build.directory}/**</exclude>
    +				<exclude>mvnw</exclude>
    +				<exclude>mvnw.cmd</exclude>
    +				<exclude>.mvn/**</exclude>
    +				<exclude>src/**</exclude>
    +			</excludes>
    +		</fileSet>
    +	</fileSets>
    +</assembly>

    88.5.2 Workflow

    The workflow would look similar to the one presented in the Step by step guide to CDC. The only difference + is that the producer doesn’t own the contracts anymore. So the consumer and the producer have to work on + common contracts in a common repository.

    88.5.3 Consumer

    When the consumer wants to work on the contracts offline, instead of cloning the producer code, the +consumer team clones the common repository, goes to the required producer’s folder (e.g. com/example/server) +and runs mvn clean install -DskipTests to install locally the stubs converted from the contracts.

    [Tip]Tip

    You need to have Maven installed locally

    88.5.4 Producer

    As a producer it’s enough to alter the Spring Cloud Contract Verifier to provide the URL and the dependency +of the JAR containing the contracts:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<configuration>
    +		<contractsMode>REMOTE</contractsMode>
    +		<contractsRepositoryUrl>
    +			https://link/to/your/nexus/or/artifactory/or/sth
    +		</contractsRepositoryUrl>
    +		<contractDependency>
    +			<groupId>com.example.standalone</groupId>
    +			<artifactId>contracts</artifactId>
    +		</contractDependency>
    +	</configuration>
    +</plugin>

    With this setup the JAR with groupid com.example.standalone and artifactid contracts will be downloaded +from http://link/to/your/nexus/or/artifactory/or/sth. It will be then unpacked in a local temporary folder +and contracts present under the com/example/server will be picked as the ones used to generate the +tests and the stubs. Due to this convention the producer team will know which consumer teams will be broken +when some incompatible changes are done.

    The rest of the flow looks the same.

    88.5.5 How can I define messaging contracts per topic not per producer?

    To avoid messaging contracts duplication in the common repo, when few producers writing messages to one topic, +we could create the structure when the rest contracts would be placed in a folder per producer and messaging +contracts in the folder per topic.

    For Maven Project

    To make it possible to work on the producer side we should specify an inclusion pattern for +filtering common repository jar by messaging topics we are interested in. includedFiles property of Maven Spring Cloud Contract plugin +allows us to do that. Also contractsPath need to be specified since the default path would be the common repository groupid/artifactid.

    <plugin>
    +   <groupId>org.springframework.cloud</groupId>
    +   <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +   <version>${spring-cloud-contract.version}</version>
    +   <configuration>
    +      <contractsMode>REMOTE</contractsMode>
    +      <contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
    +      <contractDependency>
    +         <groupId>com.example</groupId>
    +         <artifactId>common-repo-with-contracts</artifactId>
    +         <version>+</version>
    +      </contractDependency>
    +      <contractsPath>/</contractsPath>
    +      <baseClassMappings>
    +         <baseClassMapping>
    +            <contractPackageRegex>.*messaging.*</contractPackageRegex>
    +            <baseClassFQN>com.example.services.MessagingBase</baseClassFQN>
    +         </baseClassMapping>
    +         <baseClassMapping>
    +            <contractPackageRegex>.*rest.*</contractPackageRegex>
    +            <baseClassFQN>com.example.services.TestBase</baseClassFQN>
    +         </baseClassMapping>
    +      </baseClassMappings>
    +      <includedFiles>
    +         <includedFile>**/${project.artifactId}/**</includedFile>
    +         <includedFile>**/${first-topic}/**</includedFile>
    +         <includedFile>**/${second-topic}/**</includedFile>
    +      </includedFiles>
    +   </configuration>
    +</plugin>

    For Gradle Project

    • Add a custom configuration for the common-repo dependency:
    ext {
    +    conractsGroupId = "com.example"
    +    contractsArtifactId = "common-repo"
    +    contractsVersion = "1.2.3"
    +}
    +
    +configurations {
    +    contracts {
    +        transitive = false
    +    }
    +}
    • Add the common-repo dependency to your classpath:
    dependencies {
    +    contracts "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    +    testCompile "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}"
    +}
    • Download the dependency to an appropriate folder:
    task getContracts(type: Copy) {
    +    from configurations.contracts
    +    into new File(project.buildDir, "downloadedContracts")
    +}
    • Unzip JAR:
    task unzipContracts(type: Copy) {
    +    def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar")
    +    def outputDir = file("${buildDir}/unpackedContracts")
    +
    +    from zipTree(zipFile)
    +    into outputDir
    +}
    • Cleanup unused contracts:
    task deleteUnwantedContracts(type: Delete) {
    +    delete fileTree(dir: "${buildDir}/unpackedContracts",
    +        include: "**/*",
    +        excludes: [
    +            "**/${project.name}/**"",
    +            "**/${first-topic}/**",
    +            "**/${second-topic}/**"])
    +}
    • Create task dependencies:
    unzipContracts.dependsOn("getContracts")
    +deleteUnwantedContracts.dependsOn("unzipContracts")
    +build.dependsOn("deleteUnwantedContracts")
    • Configure plugin by specifying the directory containing contracts using contractsDslDir property
    contracts {
    +    contractsDslDir = new File("${buildDir}/unpackedContracts")
    +}

    88.6 Do I need a Binary Storage? Can’t I use Git?

    In the polyglot world, there are languages that don’t use binary storages like +Artifactory or Nexus. Starting from Spring Cloud Contract version 2.0.0 we provide +mechanisms to store contracts and stubs in a SCM repository. Currently the +only supported SCM is Git.

    The repository would have to the following setup +(which you can checkout here):

    .
    +└── META-INF
    +    └── com.example
    +        └── beer-api-producer-git
    +            └── 0.0.1-SNAPSHOT
    +                ├── contracts
    +                │   └── beer-api-consumer
    +                │       ├── messaging
    +                │       │   ├── shouldSendAcceptedVerification.groovy
    +                │       │   └── shouldSendRejectedVerification.groovy
    +                │       └── rest
    +                │           ├── shouldGrantABeerIfOldEnough.groovy
    +                │           └── shouldRejectABeerIfTooYoung.groovy
    +                └── mappings
    +                    └── beer-api-consumer
    +                        └── rest
    +                            ├── shouldGrantABeerIfOldEnough.json
    +                            └── shouldRejectABeerIfTooYoung.json

    Under META-INF folder:

    • we group applications via groupId (e.g. com.example)
    • then each application is represented via the artifactId (e.g. beer-api-producer-git)
    • next, the version of the application (e.g. 0.0.1-SNAPSHOT). Starting from Spring Cloud Contract version 2.1.0, you can specify the versions as follows (assuming that your versions follow the semantic versioning)

      • + or latest - to find the latest version of your stubs (assuming that the snapshots are always the latest artifact for a given revision number). That means:

        • if you have a version 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT and 2.0.0.RELEASE we will assume that the latest is 2.0.0.BUILD-SNAPSHOT
        • if you have a version 1.0.0.RELEASE and 2.0.0.RELEASE we will assume that the latest is 2.0.0.RELEASE
        • if you have a version called latest or + we will pick that folder
      • release - to find the latest release version of your stubs. That means:

        • if you have a version 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT and 2.0.0.RELEASE we will assume that the latest is 2.0.0.RELEASE
        • if you have a version called release we will pick that folder
    • finally, there are two folders:

      • contracts - the good practice is to store the contracts required by each +consumer in the folder with the consumer name (e.g. beer-api-consumer). That way you +can use the stubs-per-consumer feature. Further directory structure is arbitrary.
      • mappings - in this folder the Maven / Gradle Spring Cloud Contract plugins will push +the stub server mappings. On the consumer side, Stub Runner will scan this folder +to start stub servers with stub definitions. The folder structure will be a copy +of the one created in the contracts subfolder.

    88.6.1 Protocol convention

    In order to control the type and location of the source of contracts (whether it’s +a binary storage or an SCM repository), you can use the protocol in the URL of +the repository. Spring Cloud Contract iterates over registered protocol resolvers +and tries to fetch the contracts (via a plugin) or stubs (via Stub Runner).

    For the SCM functionality, currently, we support the Git repository. To use it, +in the property, where the repository URL needs to be placed you just have to prefix +the connection URL with git://. Here you can find a couple of examples:

    git://file:///foo/bar
    +git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git
    +git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git

    88.6.2 Producer

    For the producer, to use the SCM approach, we can reuse the +same mechanism we use for external contracts. We route Spring Cloud Contract +to use the SCM implementation via the URL that contains +the git:// protocol.

    [Important]Important

    You have to manually add the pushStubsToScm +goal in Maven or execute (bind) the pushStubsToScm task in +Gradle. We don’t push stubs to origin of your git +repository out of the box.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <!-- Base class mappings etc. -->
    +
    +        <!-- We want to pick contracts from a Git repository -->
    +        <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl>
    +
    +        <!-- We reuse the contract dependency section to set up the path
    +        to the folder that contains the contract definitions. In our case the
    +        path will be /groupId/artifactId/version/contracts -->
    +        <contractDependency>
    +            <groupId>${project.groupId}</groupId>
    +            <artifactId>${project.artifactId}</artifactId>
    +            <version>${project.version}</version>
    +        </contractDependency>
    +
    +        <!-- The contracts mode can't be classpath -->
    +        <contractsMode>REMOTE</contractsMode>
    +    </configuration>
    +    <executions>
    +        <execution>
    +            <phase>package</phase>
    +            <goals>
    +                <!-- By default we will not push the stubs back to SCM,
    +                you have to explicitly add it as a goal -->
    +                <goal>pushStubsToScm</goal>
    +            </goals>
    +        </execution>
    +    </executions>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +	// We want to pick contracts from a Git repository
    +	contractDependency {
    +		stringNotation = "${project.group}:${project.name}:${project.version}"
    +	}
    +	/*
    +	We reuse the contract dependency section to set up the path
    +	to the folder that contains the contract definitions. In our case the
    +	path will be /groupId/artifactId/version/contracts
    +	 */
    +	contractRepository {
    +		repositoryUrl = "git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git"
    +	}
    +	// The mode can't be classpath
    +	contractsMode = "REMOTE"
    +	// Base class mappings etc.
    +}
    +
    +/*
    +In this scenario we want to publish stubs to SCM whenever
    +the `publish` task is executed
    +*/
    +publish.dependsOn("publishStubsToScm")

    +

    With such a setup:

    • Git project will be cloned to a temporary directory
    • The SCM stub downloader will go to META-INF/groupId/artifactId/version/contracts folder +to find contracts. E.g. for com.example:foo:1.0.0 the path would be +META-INF/com.example/foo/1.0.0/contracts
    • Tests will be generated from the contracts
    • Stubs will be created from the contracts
    • Once the tests pass, the stubs will be committed in the cloned repository
    • Finally, a push will be done to that repo’s origin

    88.6.3 Producer with contracts stored locally

    Another option to use the SCM as the destination for stubs and contracts is to store the contracts locally, with the producer, and only push the contracts and the stubs to SCM. Below, you can find the setup required to achieve this using Maven and Gradle.

    Maven.  +

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<!-- In the default configuration, we want to use the contracts stored locally -->
    +	<configuration>
    +		<baseClassMappings>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*messaging.*</contractPackageRegex>
    +				<baseClassFQN>com.example.BeerMessagingBase</baseClassFQN>
    +			</baseClassMapping>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*rest.*</contractPackageRegex>
    +				<baseClassFQN>com.example.BeerRestBase</baseClassFQN>
    +			</baseClassMapping>
    +		</baseClassMappings>
    +		<basePackageForTests>com.example</basePackageForTests>
    +	</configuration>
    +	<executions>
    +		<execution>
    +			<phase>package</phase>
    +			<goals>
    +				<!-- By default we will not push the stubs back to SCM,
    +				you have to explicitly add it as a goal -->
    +				<goal>pushStubsToScm</goal>
    +			</goals>
    +			<configuration>
    +				<!-- We want to pick contracts from a Git repository -->
    +				<contractsRepositoryUrl>git://file://${env.ROOT}/target/contract_empty_git/
    +				</contractsRepositoryUrl>
    +				<!-- Example of URL via git protocol -->
    +				<!--<contractsRepositoryUrl>git://git@github.com:spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>-->
    +				<!-- Example of URL via http protocol -->
    +				<!--<contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>-->
    +				<!-- We reuse the contract dependency section to set up the path
    +				to the folder that contains the contract definitions. In our case the
    +				path will be /groupId/artifactId/version/contracts -->
    +				<contractDependency>
    +					<groupId>${project.groupId}</groupId>
    +					<artifactId>${project.artifactId}</artifactId>
    +					<version>${project.version}</version>
    +				</contractDependency>
    +				<!-- The mode can't be classpath -->
    +				<contractsMode>LOCAL</contractsMode>
    +			</configuration>
    +		</execution>
    +	</executions>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		// Base package for generated tests
    +	basePackageForTests = "com.example"
    +	baseClassMappings {
    +		baseClassMapping(".*messaging.*", "com.example.BeerMessagingBase")
    +		baseClassMapping(".*rest.*", "com.example.BeerRestBase")
    +	}
    +}
    +
    +/*
    +In this scenario we want to publish stubs to SCM whenever
    +the `publish` task is executed
    +*/
    +publishStubsToScm {
    +	// We want to modify the default set up of the plugin when publish stubs to scm is called
    +	customize {
    +		// We want to pick contracts from a Git repository
    +		contractDependency {
    +			stringNotation = "${project.group}:${project.name}:${project.version}"
    +		}
    +		/*
    +		We reuse the contract dependency section to set up the path
    +		to the folder that contains the contract definitions. In our case the
    +		path will be /groupId/artifactId/version/contracts
    +		 */
    +		contractRepository {
    +			repositoryUrl = "git://file://${System.getenv("ROOT")}/target/contract_empty_git/"
    +		}
    +		// The mode can't be classpath
    +		contractsMode = "LOCAL"
    +	}
    +}
    +
    +publish.dependsOn("publishStubsToScm")
    +publishToMavenLocal.dependsOn("publishStubsToScm")

    +

    With such a setup:

    • Contracts from the default src/test/resources/contracts directory will be picked
    • Tests will be generated from the contracts
    • Stubs will be created from the contracts
    • Once the tests pass

      • Git project will be cloned to a temporary directory
      • The stubs and contracts will be committed in the cloned repository
    • Finally, a push will be done to that repo’s origin

    Keeping contracts with the producer and stubs in an external repository

    It is also possible to keep the contracts in the producer repository, but keep the stubs in an external git repo. +This is most useful when you want to use the base consumer-producer collaboration flow, but do not have a possibility to +use an artifact repository for storing the stubs.

    In order to do that, use the usual producer setup, and then add the pushStubsToScm goal and set +contractsRepositoryUrl to the repository where you want to keep the stubs.

    88.6.4 Consumer

    On the consumer side when passing the repositoryRoot parameter, +either from the @AutoConfigureStubRunner annotation, the +JUnit rule, JUnit 5 extension or properties, it’s enough to pass the URL of the +SCM repository, prefixed with the protocol. For example

    @AutoConfigureStubRunner(
    +    stubsMode="REMOTE",
    +    repositoryRoot="git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git",
    +    ids="com.example:bookstore:0.0.1.RELEASE"
    +)

    With such a setup:

    • Git project will be cloned to a temporary directory
    • The SCM stub downloader will go to META-INF/groupId/artifactId/version/ folder +to find stub definitions and contracts. E.g. for com.example:foo:1.0.0 the path would be +META-INF/com.example/foo/1.0.0/
    • Stub servers will be started and fed with mappings
    • Messaging definitions will be read and used in the messaging tests

    88.7 Can I use the Pact Broker?

    When using Pact you can use the Pact Broker +to store and share Pact definitions. Starting from Spring Cloud Contract +2.0.0 one can fetch Pact files from the Pact Broker to generate +tests and stubs.

    As a prerequisite the Pact Converter and Pact Stub Downloader +are required. You have to add them via the spring-cloud-contract-pact dependency. +You can read more about it in the Section 96.1.1, “Pact Converter” section.

    [Important]Important

    Pact follows the Consumer Contract convention. That means +that the Consumer creates the Pact definitions first, then +shares the files with the Producer. Those expectations are generated +from the Consumer’s code and can break the Producer if the expectations +are not met.

    88.7.1 Pact Consumer

    The consumer uses Pact framework to generate Pact files. The +Pact files are sent to the Pact Broker. An example of such +setup can be found here.

    88.7.2 Producer

    For the producer, to use the Pact files from the Pact Broker, we can reuse the +same mechanism we use for external contracts. We route Spring Cloud Contract +to use the Pact implementation via the URL that contains +the pact:// protocol. It’s enough to pass the URL to the +Pact Broker. An example of such setup can be found here.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <!-- Base class mappings etc. -->
    +
    +        <!-- We want to pick contracts from a Git repository -->
    +        <contractsRepositoryUrl>pact://http://localhost:8085</contractsRepositoryUrl>
    +
    +        <!-- We reuse the contract dependency section to set up the path
    +        to the folder that contains the contract definitions. In our case the
    +        path will be /groupId/artifactId/version/contracts -->
    +        <contractDependency>
    +            <groupId>${project.groupId}</groupId>
    +            <artifactId>${project.artifactId}</artifactId>
    +            <!-- When + is passed, a latest tag will be applied when fetching pacts -->
    +            <version>+</version>
    +        </contractDependency>
    +
    +        <!-- The contracts mode can't be classpath -->
    +        <contractsMode>REMOTE</contractsMode>
    +    </configuration>
    +    <!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-contract-pact</artifactId>
    +            <version>${spring-cloud-contract.version}</version>
    +        </dependency>
    +    </dependencies>
    +</plugin>

    +

    Gradle.  +

    buildscript {
    +	repositories {
    +		//...
    +	}
    +
    +	dependencies {
    +		// ...
    +		// Don't forget to add spring-cloud-contract-pact to the classpath!
    +		classpath "org.springframework.cloud:spring-cloud-contract-pact:${contractVersion}"
    +	}
    +}
    +
    +contracts {
    +	// When + is passed, a latest tag will be applied when fetching pacts
    +	contractDependency {
    +		stringNotation = "${project.group}:${project.name}:+"
    +	}
    +	contractRepository {
    +		repositoryUrl = "pact://http://localhost:8085"
    +	}
    +	// The mode can't be classpath
    +	contractsMode = "REMOTE"
    +	// Base class mappings etc.
    +}

    +

    With such a setup:

    • Pact files will be downloaded from the Pact Broker
    • Spring Cloud Contract will convert the Pact files into tests and stubs
    • The JAR with the stubs gets automatically created as usual

    88.7.3 Pact Consumer (Producer Contract approach)

    In the scenario where you don’t want to do Consumer Contract approach +(for every single consumer define the expectations) but you’d prefer +to do Producer Contracts (the producer provides the contracts and +publishes stubs), it’s enough to use Spring Cloud Contract with +Stub Runner option. An example of such setup can be found here.

    First, remember to add Stub Runner and Spring Cloud Contract Pact module +as test dependencies.

    Maven.  +

    <dependencyManagement>
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-dependencies</artifactId>
    +            <version>${spring-cloud.version}</version>
    +            <type>pom</type>
    +            <scope>import</scope>
    +        </dependency>
    +    </dependencies>
    +</dependencyManagement>
    +
    +<!-- Don't forget to add spring-cloud-contract-pact to the classpath! -->
    +<dependencies>
    +    <!-- ... -->
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +        <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-contract-pact</artifactId>
    +        <scope>test</scope>
    +    </dependency>
    +</dependencies>

    +

    Gradle.  +

    dependencyManagement {
    +    imports {
    +        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    +    }
    +}
    +
    +dependencies {
    +    //...
    +    testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
    +    // Don't forget to add spring-cloud-contract-pact to the classpath!
    +    testCompile("org.springframework.cloud:spring-cloud-contract-pact")
    +}

    +

    Next, just pass the URL of the Pact Broker to repositoryRoot, prefixed +with pact:// protocol. E.g. pact://http://localhost:8085

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		ids = "com.example:beer-api-producer-pact",
    +		repositoryRoot = "pact://http://localhost:8085")
    +public class BeerControllerTest {
    +    //Inject the port of the running stub
    +    @StubRunnerPort("beer-api-producer-pact") int producerPort;
    +    //...
    +}

    With such a setup:

    • Pact files will be downloaded from the Pact Broker
    • Spring Cloud Contract will convert the Pact files into stub definitions
    • The stub servers will be started and fed with stubs

    For more information about Pact support you can go to +the Section 96.7, “Using the Pact Stub Downloader” section.

    88.8 How can I debug the request/response being sent by the generated tests client?

    The generated tests all boil down to RestAssured in some form or fashion which relies on Apache HttpClient. HttpClient has a facility called wire logging which logs the entire request and response to HttpClient. Spring Boot has a logging common application property for doing this sort of thing, just add this to your application properties

    logging.level.org.apache.http.wire=DEBUG

    88.8.1 How can I debug the mapping/request/response being sent by WireMock?

    Starting from version 1.2.0 we turn on WireMock logging to +info and the WireMock notifier to being verbose. Now you will +exactly know what request was received by WireMock server and which +matching response definition was picked.

    To turn off this feature just bump WireMock logging to ERROR

    logging.level.com.github.tomakehurst.wiremock=ERROR

    88.8.2 How can I see what got registered in the HTTP server stub?

    You can use the mappingsOutputFolder property on @AutoConfigureStubRunner, StubRunnerRule or +`StubRunnerExtension`to dump all mappings per artifact id. Also the port at which the given stub server +was started will be attached.

    88.8.3 Can I reference text from file?

    Yes! With version 1.2.0 we’ve added such a possibility. It’s enough to call file(…​) method in the +DSL and provide a path relative to where the contract lays. +If you’re using YAML just use the bodyFromFile property.

    89. Spring Cloud Contract Verifier Setup

    You can set up Spring Cloud Contract Verifier in the following ways:

    90. Add Gradle Plugin with Dependencies

    To add a Gradle plugin with dependencies, you can use code similar to the following:

    Plugin DSL GA versions.  +

    // build.gradle
    +plugins {
    +  id "groovy"
    +  // this will work only for GA versions of Spring Cloud Contract
    +  id "org.springframework.cloud.contract" version "${GAVerifierVersion}"
    +}
    +
    +dependencyManagement {
    +	imports {
    +		mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${GAVerifierVersion}"
    +	}
    +}
    +
    +dependencies {
    +	testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    +	// example with adding Spock core and Spock Spring
    +	testCompile "org.spockframework:spock-core:${spockVersion}"
    +	testCompile "org.spockframework:spock-spring:${spockVersion}"
    +	testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +}

    +

    Plugin DSL non GA versions.  +

    // settings.gradle
    +pluginManagement {
    +	plugins {
    +		id "org.springframework.cloud.contract" version "${verifierVersion}"
    +	}
    +    repositories {
    +        // to pick from local .m2
    +        mavenLocal()
    +        // for snapshots
    +        maven { url "https://repo.spring.io/snapshot" }
    +        // for milestones
    +        maven { url "https://repo.spring.io/milestone" }
    +        // for GA versions
    +        gradlePluginPortal()
    +    }
    +}
    +
    +// build.gradle
    +plugins {
    +  id "groovy"
    +  id "org.springframework.cloud.contract"
    +}
    +
    +dependencyManagement {
    +	imports {
    +		mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifierVersion}"
    +	}
    +}
    +
    +dependencies {
    +	testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    +	// example with adding Spock core and Spock Spring
    +	testCompile "org.spockframework:spock-core:${spockVersion}"
    +	testCompile "org.spockframework:spock-spring:${spockVersion}"
    +	testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +}

    +

    Legacy Plugin Application.  +

    // build.gradle
    +buildscript {
    +	repositories {
    +		mavenCentral()
    +	}
    +	dependencies {
    +		classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
    +		classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
    +        // here you can also pass additional dependencies such as Pact or Kotlin spec e.g.:
    +        // classpath "org.springframework.cloud:spring-cloud-contract-spec-kotlin:${verifier_version}"
    +	}
    +}
    +
    +apply plugin: 'groovy'
    +apply plugin: 'spring-cloud-contract'
    +
    +dependencyManagement {
    +	imports {
    +		mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifier_version}"
    +	}
    +}
    +
    +dependencies {
    +	testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    +	// example with adding Spock core and Spock Spring
    +	testCompile "org.spockframework:spock-core:${spockVersion}"
    +	testCompile "org.spockframework:spock-spring:${spockVersion}"
    +	testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +}

    +

    90.1 Gradle and Rest Assured 2.0

    By default, Rest Assured 3.x is added to the classpath. However, to use Rest Assured 2.x +you can add it to the plugins classpath, as shown here:

    buildscript {
    +	repositories {
    +		mavenCentral()
    +	}
    +	dependencies {
    +	    classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}"
    +		classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}"
    +		classpath "com.jayway.restassured:rest-assured:2.5.0"
    +		classpath "com.jayway.restassured:spring-mock-mvc:2.5.0"
    +	}
    +}
    +
    +depenendencies {
    +    // all dependencies
    +    // you can exclude rest-assured from spring-cloud-contract-verifier
    +    testCompile "com.jayway.restassured:rest-assured:2.5.0"
    +    testCompile "com.jayway.restassured:spring-mock-mvc:2.5.0"
    +}

    That way, the plugin automatically sees that Rest Assured 2.x is present on the classpath +and modifies the imports accordingly.

    90.2 Snapshot Versions for Gradle

    Add the additional snapshot repository to your build.gradle to use snapshot versions, +which are automatically uploaded after every successful build, as shown here:

    /*
    + We need to use the [buildscript {}] section when we have to modify
    + the classpath for the plugins. If that's not the case this section
    + can be skipped.
    +
    + If you don't need to modify the classpath (e.g. add a Pact dependency),
    + then you can just set the [pluginManagement {}] section in [settings.gradle] file.
    +
    + // settings.gradle
    + pluginManagement {
    +    repositories {
    +        // for snapshots
    +        maven {url "https://repo.spring.io/snapshot"}
    +        // for milestones
    +        maven {url "https://repo.spring.io/milestone"}
    +        // for GA versions
    +        gradlePluginPortal()
    +    }
    + }
    +
    + */
    +buildscript {
    +	repositories {
    +		mavenCentral()
    +		mavenLocal()
    +		maven { url "https://repo.spring.io/snapshot" }
    +		maven { url "https://repo.spring.io/milestone" }
    +		maven { url "https://repo.spring.io/release" }
    +	}
    +}

    90.3 Add stubs

    By default, Spring Cloud Contract Verifier is looking for stubs in the +src/test/resources/contracts directory.

    The directory containing stub definitions is treated as a class name, and each stub +definition is treated as a single test. Spring Cloud Contract Verifier assumes that it +contains at least one level of directories that are to be used as the test class name. +If more than one level of nested directories is present, all except the last one is used +as the package name. For example, with following structure:

    src/test/resources/contracts/myservice/shouldCreateUser.groovy
    +src/test/resources/contracts/myservice/shouldReturnUser.groovy

    Spring Cloud Contract Verifier creates a test class named defaultBasePackage.MyService +with two methods:

    • shouldCreateUser()
    • shouldReturnUser()

    90.4 Run the Plugin

    The plugin registers itself to be invoked before a check task. If you want it to be +part of your build process, you need to do nothing more. If you just want to generate +tests, invoke the generateContractTests task.

    90.5 Default Setup

    The default Gradle Plugin setup creates the following Gradle part of the build (in +pseudocode):

    contracts {
    +    testFramework ='JUNIT'
    +    testMode = 'MockMvc'
    +    generatedTestSourcesDir = project.file("${project.buildDir}/generated-test-sources/contracts")
    +    generatedTestResourcesDir = project.file("${project.buildDir}/generated-test-resources/contracts")
    +    contractsDslDir = file("${project.rootDir}/src/test/resources/contracts")
    +    basePackageForTests = 'org.springframework.cloud.verifier.tests'
    +    stubsOutputDir = project.file("${project.buildDir}/stubs")
    +
    +    // the following properties are used when you want to provide where the JAR with contract lays
    +    contractDependency {
    +        stringNotation = ''
    +    }
    +    contractsPath = ''
    +    contractsWorkOffline = false
    +    contractRepository {
    +        cacheDownloadedContracts(true)
    +    }
    +}
    +
    +tasks.create(type: Jar, name: 'verifierStubsJar', dependsOn: 'generateClientStubs') {
    +    baseName = project.name
    +    classifier = contracts.stubsSuffix
    +    from contractVerifier.stubsOutputDir
    +}
    +
    +project.artifacts {
    +    archives task
    +}
    +
    +tasks.create(type: Copy, name: 'copyContracts') {
    +    from contracts.contractsDslDir
    +    into contracts.stubsOutputDir
    +}
    +
    +verifierStubsJar.dependsOn 'copyContracts'
    +
    +publishing {
    +    publications {
    +        stubs(MavenPublication) {
    +            artifactId project.name
    +            artifact verifierStubsJar
    +        }
    +    }
    +}

    90.6 Configure Plugin

    To change the default configuration, add a contracts snippet to your Gradle config, as +shown here:

    contracts {
    +	testMode = 'MockMvc'
    +	baseClassForTests = 'org.mycompany.tests'
    +	generatedTestSourcesDir = project.file('src/generatedContract')
    +}

    90.7 Configuration Options

    • testMode: Defines the mode for acceptance tests. By default, the mode is MockMvc, +which is based on Spring’s MockMvc. It can also be changed to WebTestClient, JaxRsClient or to +Explicit for real HTTP calls.
    • imports: Creates an array with imports that should be included in generated tests +(for example ['org.myorg.Matchers']). By default, it creates an empty array.
    • staticImports: Creates an array with static imports that should be included in +generated tests(for example ['org.myorg.Matchers.*']). By default, it creates an empty +array.
    • basePackageForTests: Specifies the base package for all generated tests. If not set, +the value is picked from baseClassForTests’s package and from `packageWithBaseClasses. +If neither of these values are set, then the value is set to +org.springframework.cloud.contract.verifier.tests.
    • baseClassForTests: Creates a base class for all generated tests. By default, if you +use Spock classes, the class is spock.lang.Specification.
    • packageWithBaseClasses: Defines a package where all the base classes reside. This +setting takes precedence over baseClassForTests.
    • baseClassMappings: Explicitly maps a contract package to a FQN of a base class. This +setting takes precedence over packageWithBaseClasses and baseClassForTests.
    • ruleClassForTests: Specifies a rule that should be added to the generated test +classes.
    • ignoredFiles: Uses an Antmatcher to allow defining stub files for which processing +should be skipped. By default, it is an empty array.
    • contractsDslDir: Specifies the directory containing contracts written using the +GroovyDSL. By default, its value is $rootDir/src/test/resources/contracts.
    • generatedTestSourcesDir: Specifies the test source directory where tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-sources/contracts.
    • generatedTestResourcesDir: Specifies the test resource directory where resources used by the tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-resources/contracts.
    • stubsOutputDir: Specifies the directory where the generated WireMock stubs from +the Groovy DSL should be placed.
    • testFramework: Specifies the target test framework to be used. Currently, Spock, JUnit 4 (TestFramework.JUNIT) and +JUnit 5 are supported with JUnit 4 being the default framework.
    • contractsProperties: a map containing properties to be passed to Spring Cloud Contract +components. Those properties might be used by e.g. inbuilt or custom Stub Downloaders.

    The following properties are used when you want to specify the location of the JAR +containing the contracts:

    • contractDependency: Specifies the Dependency that provides +groupid:artifactid:version:classifier coordinates. You can use the contractDependency +closure to set it up.
    • contractsPath: Specifies the path to the jar. If contract dependencies are +downloaded, the path defaults to groupid/artifactid where groupid is slash +separated. Otherwise, it scans contracts under the provided directory.
    • contractsMode: Specifies the mode of downloading contracts (whether the +JAR is available offline, remotely etc.)
    • deleteStubsAfterTest: If set to false will not remove any downloaded +contracts from temporary directories

    Below you can find a list of experimental features you can turn on via the plugin:

    • convertToYaml: converts all DSLs to the declarative, YAML format. This can be extremely useful when you’re using external libraries in your Groovy DSLs. By turning this feature on (by setting it to true) you will not need to add the library dependency on the consumer side.
    • assertJsonSize: You can check the size of JSON arrays in the generated tests. This feature is disabled by default.

    90.8 Single Base Class for All Tests

    When using Spring Cloud Contract Verifier in default MockMvc, you need to create a base +specification for all generated acceptance tests. In this class, you need to point to an +endpoint, which should be verified.

    abstract class BaseMockMvcSpec extends Specification {
    +
    +	def setup() {
    +		RestAssuredMockMvc.standaloneSetup(new PairIdController())
    +	}
    +
    +	void isProperCorrelationId(Integer correlationId) {
    +		assert correlationId == 123456
    +	}
    +
    +	void isEmpty(String value) {
    +		assert value == null
    +	}
    +
    +}

    If you use Explicit mode, you can use a base class to initialize the whole tested app +as you might see in regular integration tests. If you use the JAXRSCLIENT mode, this +base class should also contain a protected WebTarget webTarget field. Right now, the +only option to test the JAX-RS API is to start a web server.

    90.9 Different Base Classes for Contracts

    If your base classes differ between contracts, you can tell the Spring Cloud Contract +plugin which class should get extended by the autogenerated tests. You have two options:

    • Follow a convention by providing the packageWithBaseClasses
    • Provide explicit mapping via baseClassMappings

    By Convention

    The convention is such that if you have a contract under (for example) +src/test/resources/contract/foo/bar/baz/ and set the value of the +packageWithBaseClasses property to com.example.base, then Spring Cloud Contract +Verifier assumes that there is a BarBazBase class under the com.example.base package. +In other words, the system takes the last two parts of the package, if they exist, and +forms a class with a Base suffix. This rule takes precedence over baseClassForTests. +Here is an example of how it works in the contracts closure:

    packageWithBaseClasses = 'com.example.base'

    By Mapping

    You can manually map a regular expression of the contract’s package to fully qualified +name of the base class for the matched contract. You have to provide a list called +baseClassMappings that consists baseClassMapping objects that takes a +contractPackageRegex to baseClassFQN mapping. Consider the following example:

    baseClassForTests = "com.example.FooBase"
    +baseClassMappings {
    +	baseClassMapping('.*/com/.*', 'com.example.ComBase')
    +	baseClassMapping('.*/bar/.*': 'com.example.BarBase')
    +}

    Let’s assume that you have contracts under + - src/test/resources/contract/com/ + - src/test/resources/contract/foo/

    By providing the baseClassForTests, we have a fallback in case mapping did not succeed. +(You could also provide the packageWithBaseClasses as a fallback.) That way, the tests +generated from src/test/resources/contract/com/ contracts extend the +com.example.ComBase, whereas the rest of the tests extend com.example.FooBase.

    90.10 Invoking Generated Tests

    To ensure that the provider side is compliant with defined contracts, you need to invoke:

    ./gradlew generateContractTests test

    90.11 Pushing stubs to SCM

    If you’re using the SCM repository to keep the contracts and +stubs, you might want to automate the step of pushing stubs to +the repository. To do that, it’s enough to call the pushStubsToScm +task. Example:

    $ ./gradlew pushStubsToScm

    Under Section 96.6, “Using the SCM Stub Downloader” you can find all possible +configuration options that you can pass either via +the contractsProperties field e.g. contracts { contractsProperties = [foo:"bar"] }, +via contractsProperties method e.g. contracts { contractsProperties([foo:"bar"]) }, +a system property or an environment variable.

    90.12 Spring Cloud Contract Verifier on the Consumer Side

    In a consuming service, you need to configure the Spring Cloud Contract Verifier plugin +in exactly the same way as in case of provider. If you do not want to use Stub Runner +then you need to copy contracts stored in src/test/resources/contracts and generate +WireMock JSON stubs using:

    ./gradlew generateClientStubs
    [Note]Note

    The stubsOutputDir option has to be set for stub generation to work.

    When present, JSON stubs can be used in automated tests of consuming a service.

    @ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application)
    +class LoanApplicationServiceSpec extends Specification {
    +
    + @ClassRule
    + @Shared
    + WireMockClassRule wireMockRule == new WireMockClassRule()
    +
    + @Autowired
    + LoanApplicationService sut
    +
    + def 'should successfully apply for loan'() {
    +   given:
    + 	LoanApplication application =
    +			new LoanApplication(client: new Client(clientPesel: '12345678901'), amount: 123.123)
    +   when:
    +	LoanApplicationResult loanApplication == sut.loanApplication(application)
    +   then:
    +	loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLIED
    +	loanApplication.rejectionReason == null
    + }
    +}

    LoanApplication makes a call to FraudDetection service. This request is handled by a +WireMock server configured with stubs generated by Spring Cloud Contract Verifier.

    90.13 Maven Project

    To learn how to set up the Maven project for Spring Cloud Contract Verifier, read the +following sections:

    90.13.1 Add maven plugin

    Add the Spring Cloud Contract BOM in a fashion similar to this:

    <dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>${spring-cloud-release.version}</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    Next, add the Spring Cloud Contract Verifier Maven plugin:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +		<convertToYaml>true</convertToYaml>
    +	</configuration>
    +</plugin>

    You can read more in the +Spring +Cloud Contract Maven Plugin Documentation (example for 2.0.0.RELEASE version).

    90.13.2 Maven and Rest Assured 2.0

    By default, Rest Assured 3.x is added to the classpath. However, you can use Rest +Assured 2.x by adding it to the plugins classpath, as shown here:

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <packageWithBaseClasses>com.example</packageWithBaseClasses>
    +    </configuration>
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-contract-verifier</artifactId>
    +            <version>${spring-cloud-contract.version}</version>
    +        </dependency>
    +        <dependency>
    +           <groupId>com.jayway.restassured</groupId>
    +           <artifactId>rest-assured</artifactId>
    +           <version>2.5.0</version>
    +           <scope>compile</scope>
    +        </dependency>
    +        <dependency>
    +           <groupId>com.jayway.restassured</groupId>
    +           <artifactId>spring-mock-mvc</artifactId>
    +           <version>2.5.0</version>
    +           <scope>compile</scope>
    +        </dependency>
    +    </dependencies>
    +</plugin>
    +
    +<dependencies>
    +    <!-- all dependencies -->
    +    <!-- you can exclude rest-assured from spring-cloud-contract-verifier -->
    +    <dependency>
    +       <groupId>com.jayway.restassured</groupId>
    +       <artifactId>rest-assured</artifactId>
    +       <version>2.5.0</version>
    +       <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +       <groupId>com.jayway.restassured</groupId>
    +       <artifactId>spring-mock-mvc</artifactId>
    +       <version>2.5.0</version>
    +       <scope>test</scope>
    +    </dependency>
    +</dependencies>

    That way, the plugin automatically sees that Rest Assured 3.x is present on the classpath +and modifies the imports accordingly.

    90.13.3 Snapshot versions for Maven

    For Snapshot and Milestone versions, you have to add the following section to your +pom.xml, as shown here:

    <repositories>
    +	<repository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +</repositories>
    +<pluginRepositories>
    +	<pluginRepository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +</pluginRepositories>

    90.13.4 Add stubs

    By default, Spring Cloud Contract Verifier is looking for stubs in the +src/test/resources/contracts directory. The directory containing stub definitions is +treated as a class name, and each stub definition is treated as a single test. We assume +that it contains at least one directory to be used as test class name. If there is more +than one level of nested directories, all except the last one is used as package name. +For example, with following structure:

    src/test/resources/contracts/myservice/shouldCreateUser.groovy
    +src/test/resources/contracts/myservice/shouldReturnUser.groovy

    Spring Cloud Contract Verifier creates a test class named defaultBasePackage.MyService +with two methods

    • shouldCreateUser()
    • shouldReturnUser()

    90.13.5 Run plugin

    The plugin goal generateTests is assigned to be invoked in the phase called +generate-test-sources. If you want it to be part of your build process, you need not do +anything. If you just want to generate tests, invoke the generateTests goal.

    90.13.6 Configure plugin

    To change the default configuration, just add a configuration section to the plugin +definition or the execution definition, as shown here:

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <executions>
    +        <execution>
    +            <goals>
    +                <goal>convert</goal>
    +                <goal>generateStubs</goal>
    +                <goal>generateTests</goal>
    +            </goals>
    +        </execution>
    +    </executions>
    +    <configuration>
    +        <basePackageForTests>org.springframework.cloud.verifier.twitter.place</basePackageForTests>
    +        <baseClassForTests>org.springframework.cloud.verifier.twitter.place.BaseMockMvcSpec</baseClassForTests>
    +    </configuration>
    +</plugin>

    90.13.7 Configuration Options

    • testMode: Defines the mode for acceptance tests. By default, the mode is MockMvc, +which is based on Spring’s MockMvc. It can also be changed to WebTestClient, JaxRsClient or to +Explicit for real HTTP calls.
    • basePackageForTests: Specifies the base package for all generated tests. If not set, +the value is picked from baseClassForTests’s package and from `packageWithBaseClasses. +If neither of these values are set, then the value is set to +org.springframework.cloud.contract.verifier.tests.
    • ruleClassForTests: Specifies a rule that should be added to the generated test +classes.
    • baseClassForTests: Creates a base class for all generated tests. By default, if you +use Spock classes, the class is spock.lang.Specification.
    • contractsDirectory: Specifies a directory containing contracts written with the +GroovyDSL. The default directory is /src/test/resources/contracts.
    • generatedTestSourcesDir: Specifies the test source directory where tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-sources/contracts.
    • generatedTestResourcesDir: Specifies the test resource directory where resources used by the tests generated
    • testFramework: Specifies the target test framework to be used. Currently, Spock, JUnit 4 (TestFramework.JUNIT) and +JUnit 5 are supported with JUnit 4 being the default framework.
    • packageWithBaseClasses: Defines a package where all the base classes reside. This +setting takes precedence over baseClassForTests. The convention is such that, if you +have a contract under (for example) src/test/resources/contract/foo/bar/baz/ and set +the value of the packageWithBaseClasses property to com.example.base, then Spring +Cloud Contract Verifier assumes that there is a BarBazBase class under the +com.example.base package. In other words, the system takes the last two parts of the +package, if they exist, and forms a class with a Base suffix.
    • baseClassMappings: Specifies a list of base class mappings that provide +contractPackageRegex, which is checked against the package where the contract is +located, and baseClassFQN, which maps to the fully qualified name of the base class for +the matched contract. For example, if you have a contract under +src/test/resources/contract/foo/bar/baz/ and map the property +.* → com.example.base.BaseClass, then the test class generated from these contracts +extends com.example.base.BaseClass. This setting takes precedence over +packageWithBaseClasses and baseClassForTests.
    • contractsProperties: a map containing properties to be passed to Spring Cloud Contract +components. Those properties might be used by e.g. inbuilt or custom Stub Downloaders.

    If you want to download your contract definitions from a Maven repository, you can use +the following options:

    • contractDependency: The contract dependency that contains all the packaged contracts.
    • contractsPath: The path to the concrete contracts in the JAR with packaged contracts. +Defaults to groupid/artifactid where gropuid is slash separated.
    • contractsMode: Picks the mode in which stubs will be found and registered
    • deleteStubsAfterTest: If set to false will not remove any downloaded +contracts from temporary directories
    • contractsRepositoryUrl: URL to a repo with the artifacts that have contracts. If it is not provided, +use the current Maven ones.
    • contractsRepositoryUsername: The user name to be used to connect to the repo with contracts.
    • contractsRepositoryPassword: The password to be used to connect to the repo with contracts.
    • contractsRepositoryProxyHost: The proxy host to be used to connect to the repo with contracts.
    • contractsRepositoryProxyPort: The proxy port to be used to connect to the repo with contracts.

    We cache only non-snapshot, explicitly provided versions (for example ++ or 1.0.0.BUILD-SNAPSHOT won’t get cached). By default, this feature is turned on.

    Below you can find a list of experimental features you can turn on via the plugin:

    • convertToYaml: converts all DSLs to the declarative, YAML format. This can be extremely useful when you’re using external libraries in your Groovy DSLs. By turning this feature on (by setting it to true) you will not need to add the library dependency on the consumer side.
    • assertJsonSize: You can check the size of JSON arrays in the generated tests. This feature is disabled by default.

    90.13.8 Single Base Class for All Tests

    When using Spring Cloud Contract Verifier in default MockMvc, you need to create a base +specification for all generated acceptance tests. In this class, you need to point to an +endpoint, which should be verified.

    package org.mycompany.tests
    +
    +import org.mycompany.ExampleSpringController
    +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc
    +import spock.lang.Specification
    +
    +class MvcSpec extends Specification {
    +  def setup() {
    +   RestAssuredMockMvc.standaloneSetup(new ExampleSpringController())
    +  }
    +}

    You can also setup the whole context if necessary.

    import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +import org.junit.runner.RunWith;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.boot.test.context.SpringBootTest;
    +import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.web.context.WebApplicationContext;
    +
    +@RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SomeConfig.class, properties="some=property")
    +public abstract class BaseTestClass {
    +
    +	@Autowired
    +	WebApplicationContext context;
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.webAppContextSetup(this.context);
    +	}
    +}

    If you use EXPLICIT mode, you can use a base class to initialize the whole tested app +similarly, as you might find in regular integration tests.

    import io.restassured.RestAssured;
    +import org.junit.Before;
    +import org.junit.runner.RunWith;
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.boot.test.context.SpringBootTest;
    +import org.springframework.boot.web.server.LocalServerPort
    +import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.web.context.WebApplicationContext;
    +
    +@RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SomeConfig.class, properties="some=property")
    +public abstract class BaseTestClass {
    +
    +	@LocalServerPort
    +	int port;
    +
    +	@Before
    +	public void setup() {
    +		RestAssured.baseURI = "http://localhost:" + this.port;
    +	}
    +}

    If you use the JAXRSCLIENT mode, this base class should also contain a protected WebTarget webTarget field. Right +now, the only option to test the JAX-RS API is to start a web server.

    90.13.9 Different base classes for contracts

    If your base classes differ between contracts, you can tell the Spring Cloud Contract +plugin which class should get extended by the autogenerated tests. You have two options:

    • Follow a convention by providing the packageWithBaseClasses
    • provide explicit mapping via baseClassMappings

    By Convention

    The convention is such that if you have a contract under (for example) +src/test/resources/contract/foo/bar/baz/ and set the value of the +packageWithBaseClasses property to com.example.base, then Spring Cloud Contract +Verifier assumes that there is a BarBazBase class under the com.example.base package. +In other words, the system takes the last two parts of the package, if they exist, and +forms a class with a Base suffix. This rule takes precedence over baseClassForTests. +Here is an example of how it works in the contracts closure:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<configuration>
    +		<packageWithBaseClasses>hello</packageWithBaseClasses>
    +	</configuration>
    +</plugin>

    By Mapping

    You can manually map a regular expression of the contract’s package to fully qualified +name of the base class for the matched contract. You have to provide a list called +baseClassMappings that consists baseClassMapping objects that takes a +contractPackageRegex to baseClassFQN mapping. Consider the following example:

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<configuration>
    +		<baseClassForTests>com.example.FooBase</baseClassForTests>
    +		<baseClassMappings>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*com.*</contractPackageRegex>
    +				<baseClassFQN>com.example.TestBase</baseClassFQN>
    +			</baseClassMapping>
    +		</baseClassMappings>
    +	</configuration>
    +</plugin>

    Assume that you have contracts under these two locations: +* src/test/resources/contract/com/ +* src/test/resources/contract/foo/

    By providing the baseClassForTests, we have a fallback in case mapping did not succeed. +(You can also provide the packageWithBaseClasses as a fallback.) That way, the tests +generated from src/test/resources/contract/com/ contracts extend the +com.example.ComBase, whereas the rest of the tests extend com.example.FooBase.

    90.13.10 Invoking generated tests

    The Spring Cloud Contract Maven Plugin generates verification code in a directory called +/generated-test-sources/contractVerifier and attaches this directory to testCompile +goal.

    For Groovy Spock code, use the following:

    <plugin>
    +	<groupId>org.codehaus.gmavenplus</groupId>
    +	<artifactId>gmavenplus-plugin</artifactId>
    +	<version>1.5</version>
    +	<executions>
    +		<execution>
    +			<goals>
    +				<goal>testCompile</goal>
    +			</goals>
    +		</execution>
    +	</executions>
    +	<configuration>
    +		<testSources>
    +			<testSource>
    +				<directory>${project.basedir}/src/test/groovy</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +				</includes>
    +			</testSource>
    +			<testSource>
    +				<directory>${project.build.directory}/generated-test-sources/contractVerifier</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +				</includes>
    +			</testSource>
    +		</testSources>
    +	</configuration>
    +</plugin>

    To ensure that provider side is compliant with defined contracts, you need to invoke +mvn generateTest test.

    90.13.11 Pushing stubs to SCM

    If you’re using the SCM repository to keep the contracts and +stubs, you might want to automate the step of pushing stubs to +the repository. To do that, it’s enough to add the pushStubsToScm +goal. Example:

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <!-- Base class mappings etc. -->
    +
    +        <!-- We want to pick contracts from a Git repository -->
    +        <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl>
    +
    +        <!-- We reuse the contract dependency section to set up the path
    +        to the folder that contains the contract definitions. In our case the
    +        path will be /groupId/artifactId/version/contracts -->
    +        <contractDependency>
    +            <groupId>${project.groupId}</groupId>
    +            <artifactId>${project.artifactId}</artifactId>
    +            <version>${project.version}</version>
    +        </contractDependency>
    +
    +        <!-- The contracts mode can't be classpath -->
    +        <contractsMode>REMOTE</contractsMode>
    +    </configuration>
    +    <executions>
    +        <execution>
    +            <phase>package</phase>
    +            <goals>
    +                <!-- By default we will not push the stubs back to SCM,
    +                you have to explicitly add it as a goal -->
    +                <goal>pushStubsToScm</goal>
    +            </goals>
    +        </execution>
    +    </executions>
    +</plugin>

    Under Section 96.6, “Using the SCM Stub Downloader” you can find all possible +configuration options that you can pass either via +the <configuration><contractProperties> map, a system property +or an environment variable.

    90.13.12 Maven Plugin and STS

    If you see the following exception while using STS:

    STS Exception

    When you click on the error marker you should see something like this:

     plugin:1.1.0.M1:convert:default-convert:process-test-resources) org.apache.maven.plugin.PluginExecutionException: Execution default-convert of goal org.springframework.cloud:spring-
    + cloud-contract-maven-plugin:1.1.0.M1:convert failed. at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:145) at
    + org.eclipse.m2e.core.internal.embedder.MavenImpl.execute(MavenImpl.java:331) at org.eclipse.m2e.core.internal.embedder.MavenImpl$11.call(MavenImpl.java:1362) at
    +...
    + org.eclipse.core.internal.jobs.Worker.run(Worker.java:55) Caused by: java.lang.NullPointerException at
    + org.eclipse.m2e.core.internal.builder.plexusbuildapi.EclipseIncrementalBuildContext.hasDelta(EclipseIncrementalBuildContext.java:53) at
    + org.sonatype.plexus.build.incremental.ThreadBuildContext.hasDelta(ThreadBuildContext.java:59) at

    In order to fix this issue, provide the following section in your pom.xml:

    <build>
    +    <pluginManagement>
    +        <plugins>
    +            <!--This plugin's configuration is used to store Eclipse m2e settings
    +                only. It has no influence on the Maven build itself. -->
    +            <plugin>
    +                <groupId>org.eclipse.m2e</groupId>
    +                <artifactId>lifecycle-mapping</artifactId>
    +                <version>1.0.0</version>
    +                <configuration>
    +                    <lifecycleMappingMetadata>
    +                        <pluginExecutions>
    +                             <pluginExecution>
    +                                <pluginExecutionFilter>
    +                                    <groupId>org.springframework.cloud</groupId>
    +                                    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +                                    <versionRange>[1.0,)</versionRange>
    +                                    <goals>
    +                                        <goal>convert</goal>
    +                                    </goals>
    +                                </pluginExecutionFilter>
    +                                <action>
    +                                    <execute />
    +                                </action>
    +                             </pluginExecution>
    +                        </pluginExecutions>
    +                    </lifecycleMappingMetadata>
    +                </configuration>
    +            </plugin>
    +        </plugins>
    +    </pluginManagement>
    +</build>

    90.13.13 Maven Plugin with Spock Tests

    You can select the Spock Framework for creating and executing the auto-generated contract +verification tests with both Maven and Gradle plugin. However, whereas with Gradle its really straightforward, +in Maven you will require some additional setup in order to make the tests compile and execute properly.

    First of all, you will have to use a plugin, such as GMavenPlus plugin, +to add Groovy to your project. In GMavenPlus plugin, you will need to explicitly set test sources, including both the +path where your base test classes are defined and the path were the generated contract tests are added. +Please refer to the example below:

    <plugin>
    +	<groupId>org.codehaus.gmavenplus</groupId>
    +	<artifactId>gmavenplus-plugin</artifactId>
    +	<version>1.6.1</version>
    +	<executions>
    +		<execution>
    +			<goals>
    +				<goal>compileTests</goal>
    +				<goal>addTestSources</goal>
    +			</goals>
    +		</execution>
    +	</executions>
    +	<configuration>
    +		<testSources>
    +			<testSource>
    +				<directory>${project.basedir}/src/test/groovy</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +				</includes>
    +			</testSource>
    +			<testSource>
    +				<directory>
    +					${project.basedir}/target/generated-test-sources/contracts/com/example/beer
    +				</directory>
    +				<includes>
    +					<include>**/*.groovy</include>
    +					<include>**/*.gvy</include>
    +				</includes>
    +			</testSource>
    +		</testSources>
    +	</configuration>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.codehaus.groovy</groupId>
    +			<artifactId>groovy-all</artifactId>
    +			<version>2.4.15</version>
    +			<scope>runtime</scope>
    +			<type>pom</type>
    +		</dependency>
    +	</dependencies>

    If you uphold to the Spock convention of ending the test class names with Spec, you will also need to adjust your Maven +Surefire plugin setup, like in the following example:

    90.14 Stubs and Transitive Dependencies

    The Maven and Gradle plugin that add the tasks that create the stubs jar for you. One +problem that arises is that, when reusing the stubs, you can mistakenly import all of +that stub’s dependencies. When building a Maven artifact, even though you have a couple +of different jars, all of them share one pom:

    ├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar
    +├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar.sha1
    +├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar
    +├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar.sha1
    +├── github-webhook-0.0.1.BUILD-SNAPSHOT.jar
    +├── github-webhook-0.0.1.BUILD-SNAPSHOT.pom
    +├── github-webhook-0.0.1.BUILD-SNAPSHOT-stubs.jar
    +├── ...
    +└── ...

    There are three possibilities of working with those dependencies so as not to have any +issues with transitive dependencies:

    • Mark all application dependencies as optional
    • Create a separate artifactid for the stubs
    • Exclude dependencies on the consumer side

    Mark all application dependencies as optional

    If, in the github-webhook application, you mark all of your dependencies as optional, +when you include the github-webhook stubs in another application (or when that +dependency gets downloaded by Stub Runner) then, since all of the dependencies are +optional, they will not get downloaded.

    Create a separate artifactid for the stubs

    If you create a separate artifactid, then you can set it up in whatever way you wish. +For example, you might decide to have no dependencies at all.

    Exclude dependencies on the consumer side

    As a consumer, if you add the stub dependency to your classpath, you can explicitly +exclude the unwanted dependencies.

    90.15 Scenarios

    You can handle scenarios with Spring Cloud Contract Verifier. All you need to do is to +stick to the proper naming convention while creating your contracts. The convention +requires including an order number followed by an underscore. This will work regardles + of whether you’re working with YAML or Groovy. Example:

    my_contracts_dir\
    +  scenario1\
    +    1_login.groovy
    +    2_showCart.groovy
    +    3_logout.groovy

    Such a tree causes Spring Cloud Contract Verifier to generate WireMock’s scenario with a +name of scenario1 and the three following steps:

    1. login marked as Started pointing to…​
    2. showCart marked as Step1 pointing to…​
    3. logout marked as Step2 which will close the scenario.

    More details about WireMock scenarios can be found at +https://wiremock.org/docs/stateful-behaviour/

    Spring Cloud Contract Verifier also generates tests with a guaranteed order of execution.

    90.16 Docker Project

    We’re publishing a springcloud/spring-cloud-contract Docker image +that contains a project that will generate tests and execute them in EXPLICIT mode +against a running application.

    [Tip]Tip

    The EXPLICIT mode means that the tests generated from contracts will send +real requests and not the mocked ones.

    90.16.1 Short intro to Maven, JARs and Binary storage

    Since the Docker image can be used by non JVM projects, it’s good to +explain the basic terms behind Spring Cloud Contract packaging defaults.

    Part of the following definitions were taken from the Maven Glossary

    • Project: Maven thinks in terms of projects. Everything that you +will build are projects. Those projects follow a well defined +“Project Object Model”. Projects can depend on other projects, +in which case the latter are called “dependencies”. A project may +consistent of several subprojects, however these subprojects are still +treated equally as projects.
    • Artifact: An artifact is something that is either produced or used +by a project. Examples of artifacts produced by Maven for a project +include: JARs, source and binary distributions. Each artifact +is uniquely identified by a group id and an artifact ID which is +unique within a group.
    • JAR: JAR stands for Java ARchive. It’s a format based on +the ZIP file format. Spring Cloud Contract packages the contracts and generated +stubs in a JAR file.
    • GroupId: A group ID is a universally unique identifier for a project. +While this is often just the project name (eg. commons-collections), +it is helpful to use a fully-qualified package name to distinguish it +from other projects with a similar name (eg. org.apache.maven). +Typically, when published to the Artifact Manager, the GroupId will get +slash separated and form part of the URL. E.g. for group id com.example +and artifact id application would be /com/example/application/.
    • Classifier: The Maven dependency notation looks as follows: +groupId:artifactId:version:classifier. The classifier is additional suffix +passed to the dependency. E.g. stubs, sources. The same dependency +e.g. com.example:application can produce multiple artifacts that +differ from each other with the classifier.
    • Artifact manager: When you generate binaries / sources / packages, you would +like them to be available for others to download / reference or reuse. In case +of the JVM world those artifacts would be JARs, for Ruby these are gems +and for Docker those would be Docker images. You can store those artifacts +in a manager. Examples of such managers can be Artifactory +or Nexus.

    90.16.2 How it works

    The image searches for contracts under the /contracts folder. +The output from running the tests will be available under +/spring-cloud-contract/build folder (it’s useful for debugging +purposes).

    It’s enough for you to mount your contracts, pass the environment variables + and the image will:

    • generate the contract tests
    • execute the tests against the provided URL
    • generate the WireMock stubs
    • (optional - turned on by default) publish the stubs to a Artifact Manager

    Environment Variables

    The Docker image requires some environment variables to point to +your running application, to the Artifact manager instance etc.

    • PROJECT_GROUP - your project’s group id. Defaults to com.example
    • PROJECT_VERSION - your project’s version. Defaults to 0.0.1-SNAPSHOT
    • PROJECT_NAME - artifact id. Defaults to example
    • REPO_WITH_BINARIES_URL - URL of your Artifact Manager. Defaults to http://localhost:8081/artifactory/libs-release-local +which is the default URL of Artifactory running locally
    • REPO_WITH_BINARIES_USERNAME - (optional) username when the Artifact Manager is secured
    • REPO_WITH_BINARIES_PASSWORD - (optional) password when the Artifact Manager is secured
    • PUBLISH_ARTIFACTS - if set to true then will publish artifact to binary storage. Defaults to true.

    These environment variables are used when contracts lay in an external repository. To enable +this feature you must set the EXTERNAL_CONTRACTS_ARTIFACT_ID environment variable.

    • EXTERNAL_CONTRACTS_GROUP_ID - group id of the project with contracts. Defaults to com.example
    • EXTERNAL_CONTRACTS_ARTIFACT_ID- artifact id of the project with contracts.
    • EXTERNAL_CONTRACTS_CLASSIFIER- classifier of the project with contracts. Empty by default
    • EXTERNAL_CONTRACTS_VERSION - version of the project with contracts. Defaults to +, equivalent to picking the latest
    • EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL - URL of your Artifact Manager. Defaults to value of REPO_WITH_BINARIES_URL env var. +If that’s not set, defaults to http://localhost:8081/artifactory/libs-release-local +which is the default URL of Artifactory running locally
    • EXTERNAL_CONTRACTS_PATH - path to contracts for the given project, inside the project with contracts. +Defaults to slash separated EXTERNAL_CONTRACTS_GROUP_ID concatenated with / and EXTERNAL_CONTRACTS_ARTIFACT_ID. E.g. +for group id foo.bar and artifact id baz, would result in foo/bar/baz contracts path.
    • EXTERNAL_CONTRACTS_WORK_OFFLINE - if set to true then will retrieve artifact with contracts +from the container’s .m2. Mount your local .m2 as a volume available at the container’s /root/.m2 path. +You must not set both EXTERNAL_CONTRACTS_WORK_OFFLINE and EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL.

    These environment variables are used when tests are executed:

    • APPLICATION_BASE_URL - url against which tests should be executed. +Remember that it has to be accessible from the Docker container (e.g. localhost +will not work)
    • APPLICATION_USERNAME - (optional) username for basic authentication to your application
    • APPLICATION_PASSWORD - (optional) password for basic authentication to your application

    90.16.3 Example of usage

    Let’s take a look at a simple MVC application

    $ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
    +$ cd bookstore

    The contracts are available under /contracts folder.

    90.16.4 Server side (nodejs)

    Since we want to run tests, we could just execute:

    $ npm test

    however, for learning purposes, let’s split it into pieces:

    # Stop docker infra (nodejs, artifactory)
    +$ ./stop_infra.sh
    +# Start docker infra (nodejs, artifactory)
    +$ ./setup_infra.sh
    +
    +# Kill & Run app
    +$ pkill -f "node app"
    +$ nohup node app &
    +
    +# Prepare environment variables
    +$ SC_CONTRACT_DOCKER_VERSION="..."
    +$ APP_IP="192.168.0.100"
    +$ APP_PORT="3000"
    +$ ARTIFACTORY_PORT="8081"
    +$ APPLICATION_BASE_URL="http://${APP_IP}:${APP_PORT}"
    +$ ARTIFACTORY_URL="http://${APP_IP}:${ARTIFACTORY_PORT}/artifactory/libs-release-local"
    +$ CURRENT_DIR="$( pwd )"
    +$ CURRENT_FOLDER_NAME=${PWD##*/}
    +$ PROJECT_VERSION="0.0.1.RELEASE"
    +
    +# Execute contract tests
    +$ docker run  --rm -e "APPLICATION_BASE_URL=${APPLICATION_BASE_URL}" -e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=${CURRENT_FOLDER_NAME}" -e "REPO_WITH_BINARIES_URL=${ARTIFACTORY_URL}" -e "PROJECT_VERSION=${PROJECT_VERSION}" -v "${CURRENT_DIR}/contracts/:/contracts:ro" -v "${CURRENT_DIR}/node_modules/spring-cloud-contract/output:/spring-cloud-contract-output/" springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}"
    +
    +# Kill app
    +$ pkill -f "node app"

    What will happen is that via bash scripts:

    • infrastructure will be set up (MongoDb, Artifactory). +In real life scenario you would just run the NodeJS application +with mocked database. In this example we want to show how we can +benefit from Spring Cloud Contract in no time.
    • due to those constraints the contracts also represent the +stateful situation

      • first request is a POST that causes data to get inserted to the database
      • second request is a GET that returns a list of data with 1 previously inserted element
    • the NodeJS application will be started (on port 3000)
    • contract tests will be generated via Docker and tests +will be executed against the running application

      • the contracts will be taken from /contracts folder.
      • the output of the test execution is available under +node_modules/spring-cloud-contract/output.
    • the stubs will be uploaded to Artifactory. You can check them out +under http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/ . +The stubs will be here http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/bookstore-0.0.1.RELEASE-stubs.jar.

    To see how the client side looks like check out the Section 92.9, “Stub Runner Docker” section.

    91. Spring Cloud Contract Verifier Messaging

    Spring Cloud Contract Verifier lets you verify applications that use messaging as a +means of communication. All of the integrations shown in this document work with Spring, +but you can also create one of your own and use that.

    91.1 Integrations

    You can use one of the following four integration configurations:

    • Apache Camel
    • Spring Integration
    • Spring Cloud Stream
    • Spring AMQP

    Since we use Spring Boot, if you have added one of these libraries to the classpath, all +the messaging configuration is automatically set up.

    [Important]Important

    Remember to put @AutoConfigureMessageVerifier on the base class of your +generated tests. Otherwise, messaging part of Spring Cloud Contract Verifier does not +work.

    [Important]Important

    If you want to use Spring Cloud Stream, remember to add a dependency on +org.springframework.cloud:spring-cloud-stream-test-support, as shown here:

    Maven.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-stream-test-support</artifactId>
    +    <scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile "org.springframework.cloud:spring-cloud-stream-test-support"

    +

    91.2 Manual Integration Testing

    The main interface used by the tests is +org.springframework.cloud.contract.verifier.messaging.MessageVerifier. +It defines how to send and receive messages. You can create your own implementation to +achieve the same goal.

    In a test, you can inject a ContractVerifierMessageExchange to send and receive +messages that follow the contract. Then add @AutoConfigureMessageVerifier to your test. +Here’s an example:

    @RunWith(SpringTestRunner.class)
    +@SpringBootTest
    +@AutoConfigureMessageVerifier
    +public static class MessagingContractTests {
    +
    +  @Autowired
    +  private MessageVerifier verifier;
    +  ...
    +}
    [Note]Note

    If your tests require stubs as well, then @AutoConfigureStubRunner includes the +messaging configuration, so you only need the one annotation.

    91.3 Publisher-Side Test Generation

    Having the input or outputMessage sections in your DSL results in creation of tests +on the publisher’s side. By default, JUnit 4 tests are created. However, there is also a +possibility to create JUnit 5 or Spock tests.

    There are 3 main scenarios that we should take into consideration:

    • Scenario 1: There is no input message that produces an output message. The output +message is triggered by a component inside the application (for example, scheduler).
    • Scenario 2: The input message triggers an output message.
    • Scenario 3: The input message is consumed and there is no output message.
    [Important]Important

    The destination passed to messageFrom or sentTo can have different +meanings for different messaging implementations. For Stream and Integration it is +first resolved as a destination of a channel. Then, if there is no such destination +it is resolved as a channel name. For Camel, that’s a certain component (for example, +jms).

    91.3.1 Scenario 1: No Input Message

    For the given contract:

    Groovy DSL.  +

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					triggeredBy('bookReturnedTriggered()')
    +				}
    +				outputMessage {
    +					sentTo('activemq:output')
    +					body('''{ "bookName" : "foo" }''')
    +					headers {
    +						header('BOOK-NAME', 'foo')
    +						messagingContentType(applicationJson())
    +					}
    +				}
    +			}

    +

    YAML.  +

    label: some_label
    +input:
    +  triggeredBy: bookReturnedTriggered
    +outputMessage:
    +  sentTo: activemq:output
    +  body:
    +    bookName: foo
    +  headers:
    +    BOOK-NAME: foo
    +    contentType: application/json

    +

    The following JUnit test is created:

    					'''
    + // when:
    +  bookReturnedTriggered();
    +
    + // then:
    +  ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output");
    +  assertThat(response).isNotNull();
    +  assertThat(response.getHeader("BOOK-NAME")).isNotNull();
    +  assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
    +  assertThat(response.getHeader("contentType")).isNotNull();
    +  assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");
    + // and:
    +  DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
    +  assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
    +'''

    And the following Spock test would be created:

    					'''
    + when:
    +  bookReturnedTriggered()
    +
    + then:
    +  ContractVerifierMessage response = contractVerifierMessaging.receive('activemq:output')
    +  assert response != null
    +  response.getHeader('BOOK-NAME')?.toString()  == 'foo'
    +  response.getHeader('contentType')?.toString()  == 'application/json'
    + and:
    +  DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
    +  assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
    +
    +'''

    91.3.2 Scenario 2: Output Triggered by Input

    For the given contract:

    Groovy DSL.  +

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					messageFrom('jms:input')
    +					messageBody([
    +							bookName: 'foo'
    +					])
    +					messageHeaders {
    +						header('sample', 'header')
    +					}
    +				}
    +				outputMessage {
    +					sentTo('jms:output')
    +					body([
    +							bookName: 'foo'
    +					])
    +					headers {
    +						header('BOOK-NAME', 'foo')
    +					}
    +				}
    +			}

    +

    YAML.  +

    label: some_label
    +input:
    +  messageFrom: jms:input
    +  messageBody:
    +    bookName: 'foo'
    +  messageHeaders:
    +    sample: header
    +outputMessage:
    +  sentTo: jms:output
    +  body:
    +    bookName: foo
    +  headers:
    +    BOOK-NAME: foo

    +

    The following JUnit test is created:

    					'''
    +// given:
    + ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +  "{\\"bookName\\":\\"foo\\"}"
    +, headers()
    +  .header("sample", "header"));
    +
    +// when:
    + contractVerifierMessaging.send(inputMessage, "jms:input");
    +
    +// then:
    + ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
    + assertThat(response).isNotNull();
    + assertThat(response.getHeader("BOOK-NAME")).isNotNull();
    + assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
    +// and:
    + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
    + assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
    +'''

    And the following Spock test would be created:

    					"""\
    +given:
    +   ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +    '''{"bookName":"foo"}''',
    +    ['sample': 'header']
    +  )
    +
    +when:
    +   contractVerifierMessaging.send(inputMessage, 'jms:input')
    +
    +then:
    +   ContractVerifierMessage response = contractVerifierMessaging.receive('jms:output')
    +   assert response !- null
    +   response.getHeader('BOOK-NAME')?.toString()  == 'foo'
    +and:
    +   DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
    +   assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
    +"""

    91.3.3 Scenario 3: No Output Message

    For the given contract:

    Groovy DSL.  +

    			def contractDsl = Contract.make {
    +				label 'some_label'
    +				input {
    +					messageFrom('jms:delete')
    +					messageBody([
    +							bookName: 'foo'
    +					])
    +					messageHeaders {
    +						header('sample', 'header')
    +					}
    +					assertThat('bookWasDeleted()')
    +				}
    +			}

    +

    YAML.  +

    label: some_label
    +input:
    +  messageFrom: jms:delete
    +  messageBody:
    +    bookName: 'foo'
    +  messageHeaders:
    +    sample: header
    +  assertThat: bookWasDeleted()

    +

    The following JUnit test is created:

    					'''
    +// given:
    + ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +	"{\\"bookName\\":\\"foo\\"}"
    +, headers()
    +	.header("sample", "header"));
    +
    +// when:
    + contractVerifierMessaging.send(inputMessage, "jms:delete");
    +
    +// then:
    + bookWasDeleted();
    +'''

    And the following Spock test would be created:

    					'''
    +given:
    +	 ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
    +		\'\'\'{"bookName":"foo"}\'\'\',
    +		['sample': 'header']
    +	)
    +
    +when:
    +	 contractVerifierMessaging.send(inputMessage, 'jms:delete')
    +
    +then:
    +	 noExceptionThrown()
    +	 bookWasDeleted()
    +'''

    91.4 Consumer Stub Generation

    Unlike the HTTP part, in messaging, we need to publish the Groovy DSL inside the JAR with +a stub. Then it is parsed on the consumer side and proper stubbed routes are created.

    For more information, see Chapter 93, Stub Runner for Messaging section.

    Maven.  +

    <dependencies>
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    +	</dependency>
    +
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    +		<scope>test</scope>
    +	</dependency>
    +	<dependency>
    +		<groupId>org.springframework.cloud</groupId>
    +		<artifactId>spring-cloud-stream-test-support</artifactId>
    +		<scope>test</scope>
    +	</dependency>
    +</dependencies>
    +
    +<dependencyManagement>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-dependencies</artifactId>
    +			<version>Greenwich.BUILD-SNAPSHOT</version>
    +			<type>pom</type>
    +			<scope>import</scope>
    +		</dependency>
    +	</dependencies>
    +</dependencyManagement>

    +

    Gradle.  +

    ext {
    +	contractsDir = file("mappings")
    +	stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
    +}
    +
    +// Automatically added by plugin:
    +// copyContracts - copies contracts to the output folder from which JAR will be created
    +// verifierStubsJar - JAR with a provided stub suffix
    +// the presented publication is also added by the plugin but you can modify it as you wish
    +
    +publishing {
    +	publications {
    +		stubs(MavenPublication) {
    +			artifactId "${project.name}-stubs"
    +			artifact verifierStubsJar
    +		}
    +	}
    +}

    +

    92. Spring Cloud Contract Stub Runner

    One of the issues that you might encounter while using Spring Cloud Contract Verifier is +passing the generated WireMock JSON stubs from the server side to the client side (or to +various clients). The same takes place in terms of client-side generation for messaging.

    Copying the JSON files and setting the client side for messaging manually is out of the +question. That is why we introduced Spring Cloud Contract Stub Runner. It can +automatically download and run the stubs for you.

    92.1 Snapshot versions

    Add the additional snapshot repository to your build.gradle file to use snapshot +versions, which are automatically uploaded after every successful build:

    Maven.  +

    <repositories>
    +	<repository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +	<repository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</repository>
    +</repositories>
    +<pluginRepositories>
    +	<pluginRepository>
    +		<id>spring-snapshots</id>
    +		<name>Spring Snapshots</name>
    +		<url>https://repo.spring.io/snapshot</url>
    +		<snapshots>
    +			<enabled>true</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-milestones</id>
    +		<name>Spring Milestones</name>
    +		<url>https://repo.spring.io/milestone</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +	<pluginRepository>
    +		<id>spring-releases</id>
    +		<name>Spring Releases</name>
    +		<url>https://repo.spring.io/release</url>
    +		<snapshots>
    +			<enabled>false</enabled>
    +		</snapshots>
    +	</pluginRepository>
    +</pluginRepositories>

    +

    Gradle.  +

    /*
    + We need to use the [buildscript {}] section when we have to modify
    + the classpath for the plugins. If that's not the case this section
    + can be skipped.
    +
    + If you don't need to modify the classpath (e.g. add a Pact dependency),
    + then you can just set the [pluginManagement {}] section in [settings.gradle] file.
    +
    + // settings.gradle
    + pluginManagement {
    +    repositories {
    +        // for snapshots
    +        maven {url "https://repo.spring.io/snapshot"}
    +        // for milestones
    +        maven {url "https://repo.spring.io/milestone"}
    +        // for GA versions
    +        gradlePluginPortal()
    +    }
    + }
    +
    + */
    +buildscript {
    +	repositories {
    +		mavenCentral()
    +		mavenLocal()
    +		maven { url "https://repo.spring.io/snapshot" }
    +		maven { url "https://repo.spring.io/milestone" }
    +		maven { url "https://repo.spring.io/release" }
    +	}

    +

    92.2 Publishing Stubs as JARs

    The easiest approach would be to centralize the way stubs are kept. For example, you can +keep them as jars in a Maven repository.

    [Tip]Tip

    For both Maven and Gradle, the setup comes ready to work. However, you can customize +it if you want to.

    Maven.  +

    <!-- First disable the default jar setup in the properties section -->
    +<!-- we don't want the verifier to do a jar for us -->
    +<spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>
    +
    +<!-- Next add the assembly plugin to your build -->
    +<!-- we want the assembly plugin to generate the JAR -->
    +<plugin>
    +	<groupId>org.apache.maven.plugins</groupId>
    +	<artifactId>maven-assembly-plugin</artifactId>
    +	<executions>
    +		<execution>
    +			<id>stub</id>
    +			<phase>prepare-package</phase>
    +			<goals>
    +				<goal>single</goal>
    +			</goals>
    +			<inherited>false</inherited>
    +			<configuration>
    +				<attach>true</attach>
    +				<descriptors>
    +					$../../../../src/assembly/stub.xml
    +				</descriptors>
    +			</configuration>
    +		</execution>
    +	</executions>
    +</plugin>
    +
    +<!-- Finally setup your assembly. Below you can find the contents of src/main/assembly/stub.xml -->
    +<assembly
    +	xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    +	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
    +	<id>stubs</id>
    +	<formats>
    +		<format>jar</format>
    +	</formats>
    +	<includeBaseDirectory>false</includeBaseDirectory>
    +	<fileSets>
    +		<fileSet>
    +			<directory>src/main/java</directory>
    +			<outputDirectory>/</outputDirectory>
    +			<includes>
    +				<include>**com/example/model/*.*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>${project.build.directory}/classes</directory>
    +			<outputDirectory>/</outputDirectory>
    +			<includes>
    +				<include>**com/example/model/*.*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>${project.build.directory}/snippets/stubs</directory>
    +			<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory>
    +			<includes>
    +				<include>**/*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>$../../../../src/test/resources/contracts</directory>
    +			<outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory>
    +			<includes>
    +				<include>**/*.groovy</include>
    +			</includes>
    +		</fileSet>
    +	</fileSets>
    +</assembly>

    +

    Gradle.  +

    ext {
    +	contractsDir = file("mappings")
    +	stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
    +}
    +
    +// Automatically added by plugin:
    +// copyContracts - copies contracts to the output folder from which JAR will be created
    +// verifierStubsJar - JAR with a provided stub suffix
    +// the presented publication is also added by the plugin but you can modify it as you wish
    +
    +publishing {
    +	publications {
    +		stubs(MavenPublication) {
    +			artifactId "${project.name}-stubs"
    +			artifact verifierStubsJar
    +		}
    +	}
    +}

    +

    92.3 Stub Runner Core

    Runs stubs for service collaborators. Treating stubs as contracts of services allows to use stub-runner as an implementation of +Consumer Driven Contracts.

    Stub Runner allows you to automatically download the stubs of the provided dependencies (or pick those from the classpath), start WireMock servers for them and feed them with proper stub definitions. +For messaging, special stub routes are defined.

    92.3.1 Retrieving stubs

    You can pick the following options of acquiring stubs

    • Aether based solution that downloads JARs with stubs from Artifactory / Nexus
    • Classpath scanning solution that searches classpath via pattern to retrieve stubs
    • Write your own implementation of the org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder for full customization

    The latter example is described in the Custom Stub Runner section.

    Stub downloading

    You can control the stub downloading via the stubsMode switch. It picks value from the +StubRunnerProperties.StubsMode enum. You can use the following options

    • StubRunnerProperties.StubsMode.CLASSPATH (default value) - will pick stubs from the classpath
    • StubRunnerProperties.StubsMode.LOCAL - will pick stubs from a local storage (e.g. .m2)
    • StubRunnerProperties.StubsMode.REMOTE - will pick stubs from a remote location

    Example:

    @AutoConfigureStubRunner(repositoryRoot="https://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095", stubsMode = StubRunnerProperties.StubsMode.LOCAL)

    Classpath scanning

    If you set the stubsMode property to StubRunnerProperties.StubsMode.CLASSPATH +(or set nothing since CLASSPATH is the default value) then classpath will get scanned. +Let’s look at the following example:

    @AutoConfigureStubRunner(ids = {
    +    "com.example:beer-api-producer:+:stubs:8095",
    +    "com.example.foo:bar:1.0.0:superstubs:8096"
    +})

    If you’ve added the dependencies to your classpath

    Maven.  +

    <dependency>
    +    <groupId>com.example</groupId>
    +    <artifactId>beer-api-producer-restdocs</artifactId>
    +    <classifier>stubs</classifier>
    +    <version>0.0.1-SNAPSHOT</version>
    +    <scope>test</scope>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>*</groupId>
    +            <artifactId>*</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>
    +<dependency>
    +    <groupId>com.example.foo</groupId>
    +    <artifactId>bar</artifactId>
    +    <classifier>superstubs</classifier>
    +    <version>1.0.0</version>
    +    <scope>test</scope>
    +    <exclusions>
    +        <exclusion>
    +            <groupId>*</groupId>
    +            <artifactId>*</artifactId>
    +        </exclusion>
    +    </exclusions>
    +</dependency>

    +

    Gradle.  +

    testCompile("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") {
    +    transitive = false
    +}
    +testCompile("com.example.foo:bar:1.0.0:superstubs") {
    +    transitive = false
    +}

    +

    Then the following locations on your classpath will get scanned. For com.example:beer-api-producer-restdocs

    • /META-INF/com.example/beer-api-producer-restdocs/*/.*
    • /contracts/com.example/beer-api-producer-restdocs/*/.*
    • /mappings/com.example/beer-api-producer-restdocs/*/.*

    and com.example.foo:bar

    • /META-INF/com.example.foo/bar/*/.*
    • /contracts/com.example.foo/bar/*/.*
    • /mappings/com.example.foo/bar/*/.*
    [Tip]Tip

    As you can see you have to explicitly provide the group and artifact ids when packaging the +producer stubs.

    The producer would setup the contracts like this:

    └── src
    +    └── test
    +        └── resources
    +            └── contracts
    +                └── com.example
    +                    └── beer-api-producer-restdocs
    +                        └── nested
    +                            └── contract3.groovy

    To achieve proper stub packaging.

    Or using the Maven assembly plugin or +Gradle Jar task you have to create the following +structure in your stubs jar.

    └── META-INF
    +    └── com.example
    +        └── beer-api-producer-restdocs
    +            └── 2.0.0
    +                ├── contracts
    +                │   └── nested
    +                │       └── contract2.groovy
    +                └── mappings
    +                    └── mapping.json

    By maintaining this structure classpath gets scanned and you can profit from the messaging / +HTTP stubs without the need to download artifacts.

    Configuring HTTP Server Stubs

    Stub Runner has a notion of a HttpServerStub that abstracts the underlaying +concrete implementation of the HTTP server (e.g. WireMock is one of the implementations). +Sometimes, you need to perform some additional tuning of the stub servers, +that is concrete for the given implementation. To do that, Stub Runner gives you +the httpServerStubConfigurer property that is available in the annotation, +JUnit rule, and is accessible via system properties, where you can provide +your implementation of the org.springframework.cloud.contract.stubrunner.HttpServerStubConfigurer interface. The implementations can alter +the configuration files for the given HTTP server stub.

    Spring Cloud Contract Stub Runner comes with an implementation that you +can extend, for WireMock - org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStubConfigurer. In the configure method +you can provide your own, custom configuration for the given stub. The use +case might be starting WireMock for the given artifact id, on an HTTPs port. Example:

    WireMockHttpServerStubConfigurer implementation.  +

    @CompileStatic
    +static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {
    +
    +	private static final Log log = LogFactory.getLog(HttpsForFraudDetection)
    +
    +	@Override
    +	WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
    +		if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
    +			int httpsPort = SocketUtils.findAvailableTcpPort()
    +			log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
    +			return httpStubConfiguration
    +					.httpsPort(httpsPort)
    +		}
    +		return httpStubConfiguration
    +	}
    +}

    +

    You can then reuse it via the annotation

    @AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
    +		httpServerStubConfigurer = HttpsForFraudDetection)

    Whenever an https port is found, it will take precedence over the http one.

    92.3.2 Running stubs

    Running using main app

    You can set the following options to the main class:

    -c, --classifier                Suffix for the jar containing stubs (e.
    +                                  g. 'stubs' if the stub jar would
    +                                  have a 'stubs' classifier for stubs:
    +                                  foobar-stubs ). Defaults to 'stubs'
    +                                  (default: stubs)
    +--maxPort, --maxp <Integer>     Maximum port value to be assigned to
    +                                  the WireMock instance. Defaults to
    +                                  15000 (default: 15000)
    +--minPort, --minp <Integer>     Minimum port value to be assigned to
    +                                  the WireMock instance. Defaults to
    +                                  10000 (default: 10000)
    +-p, --password                  Password to user when connecting to
    +                                  repository
    +--phost, --proxyHost            Proxy host to use for repository
    +                                  requests
    +--pport, --proxyPort [Integer]  Proxy port to use for repository
    +                                  requests
    +-r, --root                      Location of a Jar containing server
    +                                  where you keep your stubs (e.g. http:
    +                                  //nexus.
    +                                  net/content/repositories/repository)
    +-s, --stubs                     Comma separated list of Ivy
    +                                  representation of jars with stubs.
    +                                  Eg. groupid:artifactid1,groupid2:
    +                                  artifactid2:classifier
    +--sm, --stubsMode               Stubs mode to be used. Acceptable values
    +                                  [CLASSPATH, LOCAL, REMOTE]
    +-u, --username                  Username to user when connecting to
    +                                  repository

    HTTP Stubs

    Stubs are defined in JSON documents, whose syntax is defined in WireMock documentation

    Example:

    {
    +    "request": {
    +        "method": "GET",
    +        "url": "/ping"
    +    },
    +    "response": {
    +        "status": 200,
    +        "body": "pong",
    +        "headers": {
    +            "Content-Type": "text/plain"
    +        }
    +    }
    +}

    Viewing registered mappings

    Every stubbed collaborator exposes list of defined mappings under __/admin/ endpoint.

    You can also use the mappingsOutputFolder property to dump the mappings to files. + For annotation based approach it would look like this

    @AutoConfigureStubRunner(ids="a.b.c:loanIssuance,a.b.c:fraudDetectionServer",
    +mappingsOutputFolder = "target/outputmappings/")

    and for the JUnit approach like this:

    @ClassRule @Shared StubRunnerRule rule = new StubRunnerRule()
    +			.repoRoot("http://some_url")
    +			.downloadStub("a.b.c", "loanIssuance")
    +			.downloadStub("a.b.c:fraudDetectionServer")
    +			.withMappingsOutputFolder("target/outputmappings")

    Then if you check out the folder target/outputmappings you would see the following structure

    .
    +├── fraudDetectionServer_13705
    +└── loanIssuance_12255

    That means that there were two stubs registered. fraudDetectionServer was registered at port 13705 +and loanIssuance at port 12255. If we take a look at one of the files we would see (for WireMock) +mappings available for the given server:

    [{
    +  "id" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7",
    +  "request" : {
    +    "url" : "/name",
    +    "method" : "GET"
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "fraudDetectionServer"
    +  },
    +  "uuid" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7"
    +},
    +...
    +]

    Messaging Stubs

    Depending on the provided Stub Runner dependency and the DSL the messaging routes are automatically set up.

    92.4 Stub Runner JUnit Rule and Stub Runner JUnit5 Extension

    Stub Runner comes with a JUnit rule thanks to which you can very easily download and run stubs for given group and artifact id:

    @ClassRule
    +public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot())
    +		.stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")
    +		.downloadStub(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer");
    +
    +@BeforeClass
    +@AfterClass
    +public static void setupProps() {
    +	System.clearProperty("stubrunner.repository.root");
    +	System.clearProperty("stubrunner.classifier");
    +}

    There’s also a StubRunnerExtension available for JUnit 5. StubRunnerRule and StubRunnerExtension work in a very +similar fashion. After the rule/ extension is executed, Stub Runner connects to your Maven repository and for the given list of dependencies tries to:

    • download them
    • cache them locally
    • unzip them to a temporary folder
    • start a WireMock server for each Maven dependency on a random port from the provided range of ports / provided port
    • feed the WireMock server with all JSON files that are valid WireMock definitions
    • can also send messages (remember to pass an implementation of MessageVerifier interface)

    Stub Runner uses Eclipse Aether mechanism to download the Maven dependencies. +Check their docs for more information.

    Since the StubRunnerRule and StubRunnerExtension implement the StubFinder they allow you to find the started stubs:

    package org.springframework.cloud.contract.stubrunner;
    +
    +import java.net.URL;
    +import java.util.Collection;
    +import java.util.Map;
    +
    +import org.springframework.cloud.contract.spec.Contract;
    +
    +/**
    + * Contract for finding registered stubs.
    + *
    + * @author Marcin Grzejszczak
    + */
    +public interface StubFinder extends StubTrigger {
    +
    +	/**
    +	 * For the given groupId and artifactId tries to find the matching URL of the running
    +	 * stub.
    +	 * @param groupId - might be null. In that case a search only via artifactId takes
    +	 * place
    +	 * @param artifactId - artifact id of the stub
    +	 * @return URL of a running stub or throws exception if not found
    +	 * @throws StubNotFoundException in case of not finding a stub
    +	 */
    +	URL findStubUrl(String groupId, String artifactId) throws StubNotFoundException;
    +
    +	/**
    +	 * For the given Ivy notation {@code [groupId]:artifactId:[version]:[classifier]}
    +	 * tries to find the matching URL of the running stub. You can also pass only
    +	 * {@code artifactId}.
    +	 * @param ivyNotation - Ivy representation of the Maven artifact
    +	 * @return URL of a running stub or throws exception if not found
    +	 * @throws StubNotFoundException in case of not finding a stub
    +	 */
    +	URL findStubUrl(String ivyNotation) throws StubNotFoundException;
    +
    +	/**
    +	 * @return all running stubs
    +	 */
    +	RunningStubs findAllRunningStubs();
    +
    +	/**
    +	 * @return the list of Contracts
    +	 */
    +	Map<StubConfiguration, Collection<Contract>> getContracts();
    +
    +}

    Example of usage in Spock tests:

    @ClassRule
    +@Shared
    +StubRunnerRule rule = new StubRunnerRule()
    +		.stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.repoRoot(StubRunnerRuleSpec.getResource("/m2repo/repository").toURI().toString())
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance")
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
    +		.withMappingsOutputFolder("target/outputmappingsforrule")
    +
    +
    +def 'should start WireMock servers'() {
    +	expect: 'WireMocks are running'
    +		rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
    +		rule.findStubUrl('loanIssuance') != null
    +		rule.findStubUrl('loanIssuance') == rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
    +		rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
    +	and:
    +		rule.findAllRunningStubs().isPresent('loanIssuance')
    +		rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
    +		rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
    +	and: 'Stubs were registered'
    +		"${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
    +		"${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
    +}
    +
    +def 'should output mappings to output folder'() {
    +	when:
    +		def url = rule.findStubUrl('fraudDetectionServer')
    +	then:
    +		new File("target/outputmappingsforrule", "fraudDetectionServer_${url.port}").exists()
    +}

    Example of usage in JUnit tests:

    	@Test
    +	public void should_start_wiremock_servers() throws Exception {
    +		// expect: 'WireMocks are running'
    +		then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")).isNotNull();
    +		then(rule.findStubUrl("loanIssuance")).isNotNull();
    +		then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl(
    +				"org.springframework.cloud.contract.verifier.stubs", "loanIssuance"));
    +		then(rule.findStubUrl(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer"))
    +						.isNotNull();
    +		// and:
    +		then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue();
    +		then(rule.findAllRunningStubs().isPresent(
    +				"org.springframework.cloud.contract.verifier.stubs",
    +				"fraudDetectionServer")).isTrue();
    +		then(rule.findAllRunningStubs().isPresent(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer"))
    +						.isTrue();
    +		// and: 'Stubs were registered'
    +		then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name"))
    +				.isEqualTo("loanIssuance");
    +		then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name"))
    +				.isEqualTo("fraudDetectionServer");
    +	}
    +
    +	private String httpGet(String url) throws Exception {
    +		try (InputStream stream = URI.create(url).toURL().openStream()) {
    +			return StreamUtils.copyToString(stream, Charset.forName("UTF-8"));
    +		}
    +	}
    +
    +}

    JUnit 5 Extension example:

    // Visible for Junit
    +@RegisterExtension
    +static StubRunnerExtension stubRunnerExtension = new StubRunnerExtension()
    +		.repoRoot(repoRoot()).stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")
    +		.downloadStub(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
    +		.withMappingsOutputFolder("target/outputmappingsforrule");
    +
    +@BeforeAll
    +@AfterAll
    +static void setupProps() {
    +	System.clearProperty("stubrunner.repository.root");
    +	System.clearProperty("stubrunner.classifier");
    +}
    +
    +private static String repoRoot() {
    +	try {
    +		return StubRunnerRuleJUnitTest.class.getResource("/m2repo/repository/")
    +				.toURI().toString();
    +	}
    +	catch (Exception e) {
    +		return "";
    +	}
    +}

    Check the Common properties for JUnit and Spring for more information on how to apply global configuration of Stub Runner.

    [Important]Important

    To use the JUnit rule or JUnit 5 extension together with messaging, you have to provide an implementation of the +MessageVerifier interface to the rule builder (e.g. rule.messageVerifier(new MyMessageVerifier())). +If you don’t do this, then whenever you try to send a message an exception will be thrown.

    92.4.1 Maven settings

    The stub downloader honors Maven settings for a different local repository folder. +Authentication details for repositories and profiles are currently not taken into account, so you need to specify it using the properties mentioned above.

    92.4.2 Providing fixed ports

    You can also run your stubs on fixed ports. You can do it in two different ways. One is to pass it in the properties, and the other via fluent API of +JUnit rule.

    92.4.3 Fluent API

    When using the StubRunnerRule or StubRunnerExtension you can add a stub to download and then pass the port for the last downloaded stub.

    @ClassRule
    +public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot())
    +		.stubsMode(StubRunnerProperties.StubsMode.REMOTE)
    +		.downloadStub("org.springframework.cloud.contract.verifier.stubs",
    +				"loanIssuance")
    +		.withPort(12345).downloadStub(
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer:12346");
    +
    +@BeforeClass
    +@AfterClass
    +public static void setupProps() {
    +	System.clearProperty("stubrunner.repository.root");
    +	System.clearProperty("stubrunner.classifier");
    +}

    You can see that for this example the following test is valid:

    then(rule.findStubUrl("loanIssuance"))
    +		.isEqualTo(URI.create("http://localhost:12345").toURL());
    +then(rule.findStubUrl("fraudDetectionServer"))
    +		.isEqualTo(URI.create("http://localhost:12346").toURL());

    92.4.4 Stub Runner with Spring

    Sets up Spring configuration of the Stub Runner project.

    By providing a list of stubs inside your configuration file the Stub Runner automatically downloads +and registers in WireMock the selected stubs.

    If you want to find the URL of your stubbed dependency you can autowire the StubFinder interface and use +its methods as presented below:

    @ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
    +@SpringBootTest(properties = [" stubrunner.cloud.enabled=false",
    +		'foo=${stubrunner.runningstubs.fraudDetectionServer.port}',
    +		'fooWithGroup=${stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port}'])
    +@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
    +		httpServerStubConfigurer = HttpsForFraudDetection)
    +@ActiveProfiles("test")
    +class StubRunnerConfigurationSpec extends Specification {
    +
    +	@Autowired
    +	StubFinder stubFinder
    +	@Autowired
    +	Environment environment
    +	@StubRunnerPort("fraudDetectionServer")
    +	int fraudDetectionServerPort
    +	@StubRunnerPort("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
    +	int fraudDetectionServerPortWithGroupId
    +	@Value('${foo}')
    +	Integer foo
    +
    +	@BeforeClass
    +	@AfterClass
    +	void setupProps() {
    +		System.clearProperty("stubrunner.repository.root")
    +		System.clearProperty("stubrunner.classifier")
    +		WireMockHttpServerStubAccessor.clear()
    +	}
    +
    +	def 'should mark all ports as random'() {
    +		expect:
    +			WireMockHttpServerStubAccessor.everyPortRandom()
    +	}
    +
    +	def 'should start WireMock servers'() {
    +		expect: 'WireMocks are running'
    +			stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
    +			stubFinder.findStubUrl('loanIssuance') != null
    +			stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
    +			stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance')
    +			stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs')
    +			stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
    +		and:
    +			stubFinder.findAllRunningStubs().isPresent('loanIssuance')
    +			stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
    +			stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
    +		and: 'Stubs were registered'
    +			"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
    +			"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
    +		and: 'Fraud Detection is an HTTPS endpoint'
    +			stubFinder.findStubUrl('fraudDetectionServer').toString().startsWith("https")
    +	}
    +
    +	def 'should throw an exception when stub is not found'() {
    +		when:
    +			stubFinder.findStubUrl('nonExistingService')
    +		then:
    +			thrown(StubNotFoundException)
    +		when:
    +			stubFinder.findStubUrl('nonExistingGroupId', 'nonExistingArtifactId')
    +		then:
    +			thrown(StubNotFoundException)
    +	}
    +
    +	def 'should register started servers as environment variables'() {
    +		expect:
    +			environment.getProperty("stubrunner.runningstubs.loanIssuance.port") != null
    +			stubFinder.findAllRunningStubs().getPort("loanIssuance") == (environment.getProperty("stubrunner.runningstubs.loanIssuance.port") as Integer)
    +		and:
    +			environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
    +			stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") as Integer)
    +		and:
    +			environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
    +			stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port") as Integer)
    +	}
    +
    +	def 'should be able to interpolate a running stub in the passed test property'() {
    +		given:
    +			int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
    +		expect:
    +			fraudPort > 0
    +			environment.getProperty("foo", Integer) == fraudPort
    +			environment.getProperty("fooWithGroup", Integer) == fraudPort
    +			foo == fraudPort
    +	}
    +
    +	@Issue("#573")
    +	def 'should be able to retrieve the port of a running stub via an annotation'() {
    +		given:
    +			int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
    +		expect:
    +			fraudPort > 0
    +			fraudDetectionServerPort == fraudPort
    +			fraudDetectionServerPortWithGroupId == fraudPort
    +	}
    +
    +	def 'should dump all mappings to a file'() {
    +		when:
    +			def url = stubFinder.findStubUrl("fraudDetectionServer")
    +		then:
    +			new File("target/outputmappings/", "fraudDetectionServer_${url.port}").exists()
    +	}
    +
    +	@Configuration
    +	@EnableAutoConfiguration
    +	static class Config {}
    +
    +	@CompileStatic
    +	static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {
    +
    +		private static final Log log = LogFactory.getLog(HttpsForFraudDetection)
    +
    +		@Override
    +		WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
    +			if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
    +				int httpsPort = SocketUtils.findAvailableTcpPort()
    +				log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
    +				return httpStubConfiguration
    +						.httpsPort(httpsPort)
    +			}
    +			return httpStubConfiguration
    +		}
    +	}
    +}

    for the following configuration file:

    stubrunner:
    +  repositoryRoot: classpath:m2repo/repository/
    +  ids:
    +    - org.springframework.cloud.contract.verifier.stubs:loanIssuance
    +    - org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer
    +    - org.springframework.cloud.contract.verifier.stubs:bootService
    +  stubs-mode: remote

    Instead of using the properties you can also use the properties inside the @AutoConfigureStubRunner. +Below you can find an example of achieving the same result by setting values on the annotation.

    @AutoConfigureStubRunner(
    +		ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
    +				"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
    +				"org.springframework.cloud.contract.verifier.stubs:bootService"],
    +		stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		repositoryRoot = "classpath:m2repo/repository/")

    Stub Runner Spring registers environment variables in the following manner +for every registered WireMock server. Example for Stub Runner ids + com.example:foo, com.example:bar.

    • stubrunner.runningstubs.foo.port
    • stubrunner.runningstubs.com.example.foo.port
    • stubrunner.runningstubs.bar.port
    • stubrunner.runningstubs.com.example.bar.port

    Which you can reference in your code.

    You can also use the @StubRunnerPort annotation to inject the port of a running stub. +Value of the annotation can be the groupid:artifactid or just the artifactid. Example for Stub Runner ids +com.example:foo, com.example:bar.

    @StubRunnerPort("foo")
    +int fooPort;
    +@StubRunnerPort("com.example:bar")
    +int barPort;

    92.5 Stub Runner Spring Cloud

    Stub Runner can integrate with Spring Cloud.

    For real life examples you can check the

    92.5.1 Stubbing Service Discovery

    The most important feature of Stub Runner Spring Cloud is the fact that it’s stubbing

    • DiscoveryClient
    • Ribbon ServerList

    that means that regardless of the fact whether you’re using Zookeeper, Consul, Eureka or anything else, you don’t need that in your tests. +We’re starting WireMock instances of your dependencies and we’re telling your application whenever you’re using Feign, load balanced RestTemplate +or DiscoveryClient directly, to call those stubbed servers instead of calling the real Service Discovery tool.

    For example this test will pass

    def 'should make service discovery work'() {
    +	expect: 'WireMocks are running'
    +		"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
    +		"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
    +	and: 'Stubs can be reached via load service discovery'
    +		restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
    +		restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
    +}

    for the following configuration file

    stubrunner:
    +  idsToServiceIds:
    +    ivyNotation: someValueInsideYourCode
    +    fraudDetectionServer: someNameThatShouldMapFraudDetectionServer

    Test profiles and service discovery

    In your integration tests you typically don’t want to call neither a discovery service (e.g. Eureka) +or Config Server. That’s why you create an additional test configuration in which you want to disable +these features.

    Due to certain limitations of spring-cloud-commons to achieve this you have disable these properties +via a static block like presented below (example for Eureka)

        //Hack to work around https://github.com/spring-cloud/spring-cloud-commons/issues/156
    +    static {
    +        System.setProperty("eureka.client.enabled", "false");
    +        System.setProperty("spring.cloud.config.failFast", "false");
    +    }

    92.5.2 Additional Configuration

    You can match the artifactId of the stub with the name of your app by using the stubrunner.idsToServiceIds: map. +You can disable Stub Runner Ribbon support by providing: stubrunner.cloud.ribbon.enabled equal to false +You can disable Stub Runner support by providing: stubrunner.cloud.enabled equal to false

    [Tip]Tip

    By default all service discovery will be stubbed. That means that regardless of the fact if you have +an existing DiscoveryClient its results will be ignored. However, if you want to reuse it, just set + stubrunner.cloud.delegate.enabled to true and then your existing DiscoveryClient results will be + merged with the stubbed ones.

    The default Maven configuration used by Stub Runner can be tweaked either +via the following system properties or environment variables

    • maven.repo.local - path to the custom maven local repository location
    • org.apache.maven.user-settings - path to custom maven user settings location
    • org.apache.maven.global-settings - path to maven global settings location

    92.6 Stub Runner Boot Application

    Spring Cloud Contract Stub Runner Boot is a Spring Boot application that exposes REST endpoints to +trigger the messaging labels and to access started WireMock servers.

    One of the use-cases is to run some smoke (end to end) tests on a deployed application. +You can check out the Spring Cloud Pipelines +project for more information.

    92.6.1 How to use it?

    Stub Runner Server

    Just add the

    compile "org.springframework.cloud:spring-cloud-starter-stub-runner"

    Annotate a class with @EnableStubRunnerServer, build a fat-jar and you’re ready to go!

    For the properties check the Stub Runner Spring section.

    Stub Runner Server Fat Jar

    You can download a standalone JAR from Maven (e.g. for version 2.0.1.RELEASE), as follows:

    $ wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.0.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.0.1.RELEASE.jar'
    +$ java -jar stub-runner.jar --stubrunner.ids=... --stubrunner.repositoryRoot=...

    Spring Cloud CLI

    Starting from 1.4.0.RELEASE version of the Spring Cloud CLI +project you can start Stub Runner Boot by executing spring cloud stubrunner.

    In order to pass the configuration just create a stubrunner.yml file in the current working directory +or a subdirectory called config or in ~/.spring-cloud. The file could look like this +(example for running stubs installed locally)

    stubrunner.yml.  +

    stubrunner:
    +  stubsMode: LOCAL
    +  ids:
    +    - com.example:beer-api-producer:+:9876

    +

    and then just call spring cloud stubrunner from your terminal window to start +the Stub Runner server. It will be available at port 8750.

    92.6.2 Endpoints

    HTTP

    • GET /stubs - returns a list of all running stubs in ivy:integer notation
    • GET /stubs/{ivy} - returns a port for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    Messaging

    For Messaging

    • GET /triggers - returns a list of all running labels in ivy : [ label1, label2 …​] notation
    • POST /triggers/{label} - executes a trigger with label
    • POST /triggers/{ivy}/{label} - executes a trigger with label for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

    92.6.3 Example

    @ContextConfiguration(classes = StubRunnerBoot, loader = SpringBootContextLoader)
    +@SpringBootTest(properties = "spring.cloud.zookeeper.enabled=false")
    +@ActiveProfiles("test")
    +class StubRunnerBootSpec extends Specification {
    +
    +	@Autowired
    +	StubRunning stubRunning
    +
    +	def setup() {
    +		RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
    +				new TriggerController(stubRunning))
    +	}
    +
    +	def 'should return a list of running stub servers in "full ivy:port" notation'() {
    +		when:
    +			String response = RestAssuredMockMvc.get('/stubs').body.asString()
    +		then:
    +			def root = new JsonSlurper().parseText(response)
    +			root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer
    +	}
    +
    +	def 'should return a port on which a [#stubId] stub is running'() {
    +		when:
    +			def response = RestAssuredMockMvc.get("/stubs/${stubId}")
    +		then:
    +			response.statusCode == 200
    +			Integer.valueOf(response.body.asString()) > 0
    +		where:
    +			stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs',
    +					   'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs',
    +					   'org.springframework.cloud.contract.verifier.stubs:bootService:+',
    +					   'org.springframework.cloud.contract.verifier.stubs:bootService',
    +					   'bootService']
    +	}
    +
    +	def 'should return 404 when missing stub was called'() {
    +		when:
    +			def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
    +		then:
    +			response.statusCode == 404
    +	}
    +
    +	def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
    +		when:
    +			String response = RestAssuredMockMvc.get('/triggers').body.asString()
    +		then:
    +			def root = new JsonSlurper().parseText(response)
    +			root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book", "return_book_1", "return_book_2"])
    +	}
    +
    +	def 'should trigger a messaging label'() {
    +		given:
    +			StubRunning stubRunning = Mock()
    +			RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
    +		when:
    +			def response = RestAssuredMockMvc.post("/triggers/delete_book")
    +		then:
    +			response.statusCode == 200
    +		and:
    +			1 * stubRunning.trigger('delete_book')
    +	}
    +
    +	def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() {
    +		given:
    +			StubRunning stubRunning = Mock()
    +			RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
    +		when:
    +			def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book")
    +		then:
    +			response.statusCode == 200
    +		and:
    +			1 * stubRunning.trigger(stubId, 'delete_book')
    +		where:
    +			stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService']
    +	}
    +
    +	def 'should throw exception when trigger is missing'() {
    +		when:
    +			RestAssuredMockMvc.post("/triggers/missing_label")
    +		then:
    +			Exception e = thrown(Exception)
    +			e.message.contains("Exception occurred while trying to return [missing_label] label.")
    +			e.message.contains("Available labels are")
    +			e.message.contains("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]")
    +			e.message.contains("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=")
    +	}
    +
    +}

    92.6.4 Stub Runner Boot with Service Discovery

    One of the possibilities of using Stub Runner Boot is to use it as a feed of stubs for "smoke-tests". What does it mean? + Let’s assume that you don’t want to deploy 50 microservice to a test environment in order + to check if your application is working fine. You’ve already executed a suite of tests during the build process + but you would also like to ensure that the packaging of your application is fine. What you can do + is to deploy your application to an environment, start it and run a couple of tests on it to see if + it’s working fine. We can call those tests smoke-tests since their idea is to check only a handful + of testing scenarios.

    The problem with this approach is such that if you’re doing microservices most likely you’re + using a service discovery tool. Stub Runner Boot allows you to solve this issue by starting the + required stubs and register them in a service discovery tool. Let’s take a look at an example of + such a setup with Eureka. Let’s assume that Eureka was already running.

    @SpringBootApplication
    +@EnableStubRunnerServer
    +@EnableEurekaClient
    +@AutoConfigureStubRunner
    +public class StubRunnerBootEurekaExample {
    +
    +	public static void main(String[] args) {
    +		SpringApplication.run(StubRunnerBootEurekaExample.class, args);
    +	}
    +
    +}

    As you can see we want to start a Stub Runner Boot server @EnableStubRunnerServer, enable Eureka client @EnableEurekaClient +and we want to have the stub runner feature turned on @AutoConfigureStubRunner.

    Now let’s assume that we want to start this application so that the stubs get automatically registered. + We can do it by running the app java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar where + ${SYSTEM_PROPS} would contain the following list of properties

    * -Dstubrunner.repositoryRoot=https://repo.spring.io/snapshot (1)
    +* -Dstubrunner.cloud.stubbed.discovery.enabled=false (2)
    +* -Dstubrunner.ids=org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.
    +* springframework.cloud.contract.verifier.stubs:fraudDetectionServer,org.springframework.
    +* cloud.contract.verifier.stubs:bootService (3)
    +* -Dstubrunner.idsToServiceIds.fraudDetectionServer=
    +* someNameThatShouldMapFraudDetectionServer (4)
    +*
    +* (1) - we tell Stub Runner where all the stubs reside (2) - we don't want the default
    +* behaviour where the discovery service is stubbed. That's why the stub registration will
    +* be picked (3) - we provide a list of stubs to download (4) - we provide a list of

    That way your deployed application can send requests to started WireMock servers via the service +discovery. Most likely points 1-3 could be set by default in application.yml cause they are not +likely to change. That way you can provide only the list of stubs to download whenever you start +the Stub Runner Boot.

    92.7 Stubs Per Consumer

    There are cases in which 2 consumers of the same endpoint want to have 2 different responses.

    [Tip]Tip

    This approach also allows you to immediately know which consumer is using which part of your API. +You can remove part of a response that your API produces and you can see which of your autogenerated tests +fails. If none fails then you can safely delete that part of the response cause nobody is using it.

    Let’s look at the following example for contract defined for the producer called producer. +There are 2 consumers: foo-consumer and bar-consumer.

    Consumer foo-service

    request {
    +   url '/foo'
    +   method GET()
    +}
    +response {
    +    status OK()
    +    body(
    +       foo: "foo"
    +    }
    +}

    Consumer bar-service

    request {
    +   url '/foo'
    +   method GET()
    +}
    +response {
    +    status OK()
    +    body(
    +       bar: "bar"
    +    }
    +}

    You can’t produce for the same request 2 different responses. That’s why you can properly package the +contracts and then profit from the stubsPerConsumer feature.

    On the producer side the consumers can have a folder that contains contracts related only to them. +By setting the stubrunner.stubs-per-consumer flag to true we no longer register all stubs but only those that +correspond to the consumer application’s name. In other words we’ll scan the path of every stub and +if it contains the subfolder with name of the consumer in the path only then will it get registered.

    On the foo producer side the contracts would look like this

    .
    +└── contracts
    +    ├── bar-consumer
    +    │   ├── bookReturnedForBar.groovy
    +    │   └── shouldCallBar.groovy
    +    └── foo-consumer
    +        ├── bookReturnedForFoo.groovy
    +        └── shouldCallFoo.groovy

    Being the bar-consumer consumer you can either set the spring.application.name or the stubrunner.consumer-name to bar-consumer +Or set the test as follows:

    @ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
    +@SpringBootTest(properties = ["spring.application.name=bar-consumer"])
    +@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
    +		repositoryRoot = "classpath:m2repo/repository/",
    +		stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		stubsPerConsumer = true)
    +class StubRunnerStubsPerConsumerSpec extends Specification {
    +...
    +}

    Then only the stubs registered under a path that contains the bar-consumer in its name (i.e. those from the +src/test/resources/contracts/bar-consumer/some/contracts/…​ folder) will be allowed to be referenced.

    Or set the consumer name explicitly

    @ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
    +@SpringBootTest
    +@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
    +		repositoryRoot = "classpath:m2repo/repository/",
    +		consumerName = "foo-consumer",
    +		stubsMode = StubRunnerProperties.StubsMode.REMOTE,
    +		stubsPerConsumer = true)
    +class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification {
    +...
    +}

    Then only the stubs registered under a path that contains the foo-consumer in its name (i.e. those from the +src/test/resources/contracts/foo-consumer/some/contracts/…​ folder) will be allowed to be referenced.

    You can check out issue 224 for more +information about the reasons behind this change.

    92.8 Common

    This section briefly describes common properties, including:

    92.8.1 Common Properties for JUnit and Spring

    You can set repetitive properties by using system properties or Spring configuration +properties. Here are their names with their default values:

    Property nameDefault valueDescription

    stubrunner.minPort

    10000

    Minimum value of a port for a started WireMock with stubs.

    stubrunner.maxPort

    15000

    Maximum value of a port for a started WireMock with stubs.

    stubrunner.repositoryRoot

     

    Maven repo URL. If blank, then call the local maven repo.

    stubrunner.classifier

    stubs

    Default classifier for the stub artifacts.

    stubrunner.stubsMode

    CLASSPATH

    The way you want to fetch and register the stubs

    stubrunner.ids

     

    Array of Ivy notation stubs to download.

    stubrunner.username

     

    Optional username to access the tool that stores the JARs with +stubs.

    stubrunner.password

     

    Optional password to access the tool that stores the JARs with +stubs.

    stubrunner.stubsPerConsumer

    false

    Set to true if you want to use different stubs for +each consumer instead of registering all stubs for every consumer.

    stubrunner.consumerName

     

    If you want to use a stub for each consumer and want to +override the consumer name just change this value.

    92.8.2 Stub Runner Stubs IDs

    You can provide the stubs to download via the stubrunner.ids system property. They +follow this pattern:

    groupId:artifactId:version:classifier:port

    Note that version, classifier and port are optional.

    • If you do not provide the port, a random one will be picked.
    • If you do not provide the classifier, the default is used. (Note that you can +pass an empty classifier this way: groupId:artifactId:version:).
    • If you do not provide the version, then the + will be passed and the latest one is +downloaded.

    port means the port of the WireMock server.

    [Important]Important

    Starting with version 1.0.4, you can provide a range of versions that you +would like the Stub Runner to take into consideration. You can read more about the +Aether versioning +ranges here.

    92.9 Stub Runner Docker

    We’re publishing a spring-cloud/spring-cloud-contract-stub-runner Docker image +that will start the standalone version of Stub Runner.

    If you want to learn more about the basics of Maven, artifact ids, +group ids, classifiers and Artifact Managers, just click here Section 90.16, “Docker Project”.

    92.9.1 How to use it

    Just execute the docker image. You can pass any of the Section 92.8.1, “Common Properties for JUnit and Spring” +as environment variables. The convention is that all the +letters should be upper case. The camel case notation should +and the dot (.) should be separated via underscore (_). E.g. + the stubrunner.repositoryRoot property should be represented + as a STUBRUNNER_REPOSITORY_ROOT environment variable.

    92.9.2 Example of client side usage in a non JVM project

    We’d like to use the stubs created in this Section 90.16.4, “Server side (nodejs)” step. +Let’s assume that we want to run the stubs on port 9876. The NodeJS code +is available here:

    $ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
    +$ cd bookstore

    Let’s run the Stub Runner Boot application with the stubs.

    # Provide the Spring Cloud Contract Docker version
    +$ SC_CONTRACT_DOCKER_VERSION="..."
    +# The IP at which the app is running and Docker container can reach it
    +$ APP_IP="192.168.0.100"
    +# Spring Cloud Contract Stub Runner properties
    +$ STUBRUNNER_PORT="8083"
    +# Stub coordinates 'groupId:artifactId:version:classifier:port'
    +$ STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876"
    +$ STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local"
    +# Run the docker with Stub Runner Boot
    +$ docker run  --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" -e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" -e "STUBRUNNER_STUBS_MODE=REMOTE" -p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"

    What’s happening is that

    • a standalone Stub Runner application got started
    • it downloaded the stub with coordinates com.example:bookstore:0.0.1.RELEASE:stubs on port 9876
    • it got downloaded from Artifactory running at http://192.168.0.100:8081/artifactory/libs-release-local
    • after a while Stub Runner will be running on port 8083
    • and the stubs will be running at port 9876

    On the server side we built a stateful stub. Let’s use curl to assert +that the stubs are setup properly.

    # let's execute the first request (no response is returned)
    +$ curl -H "Content-Type:application/json" -X POST --data '{ "title" : "Title", "genre" : "Genre", "description" : "Description", "author" : "Author", "publisher" : "Publisher", "pages" : 100, "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg", "buy_url" : "https://pivotal.io" }' http://localhost:9876/api/books
    +# Now time for the second request
    +$ curl -X GET http://localhost:9876/api/books
    +# You will receive contents of the JSON
    [Important]Important

    If you want use the stubs that you have built locally, on your host, +then you should pass the environment variable -e STUBRUNNER_STUBS_MODE=LOCAL and mount +the volume of your local m2 -v "${HOME}/.m2/:/root/.m2:ro"

    93. Stub Runner for Messaging

    Stub Runner can run the published stubs in memory. It can integrate with the following +frameworks:

    • Spring Integration
    • Spring Cloud Stream
    • Apache Camel
    • Spring AMQP

    It also provides entry points to integrate with any other solution on the market.

    [Important]Important

    If you have multiple frameworks on the classpath Stub Runner will need to +define which one should be used. Let’s assume that you have both AMQP, Spring Cloud Stream and Spring Integration +on the classpath. Then you need to set stubrunner.stream.enabled=false and stubrunner.integration.enabled=false. +That way the only remaining framework is Spring AMQP.

    93.1 Stub triggering

    To trigger a message, use the StubTrigger interface:

    package org.springframework.cloud.contract.stubrunner;
    +
    +import java.util.Collection;
    +import java.util.Map;
    +
    +/**
    + * Contract for triggering stub messages.
    + *
    + * @author Marcin Grzejszczak
    + */
    +public interface StubTrigger {
    +
    +	/**
    +	 * Triggers an event by a given label for a given {@code groupid:artifactid} notation.
    +	 * You can use only {@code artifactId} too.
    +	 *
    +	 * Feature related to messaging.
    +	 * @param ivyNotation ivy notation of a stub
    +	 * @param labelName name of the label to trigger
    +	 * @return true - if managed to run a trigger
    +	 */
    +	boolean trigger(String ivyNotation, String labelName);
    +
    +	/**
    +	 * Triggers an event by a given label.
    +	 *
    +	 * Feature related to messaging.
    +	 * @param labelName name of the label to trigger
    +	 * @return true - if managed to run a trigger
    +	 */
    +	boolean trigger(String labelName);
    +
    +	/**
    +	 * Triggers all possible events.
    +	 *
    +	 * Feature related to messaging.
    +	 * @return true - if managed to run a trigger
    +	 */
    +	boolean trigger();
    +
    +	/**
    +	 * Feature related to messaging.
    +	 * @return a mapping of ivy notation of a dependency to all the labels it has.
    +	 */
    +	Map<String, Collection<String>> labels();
    +
    +}

    For convenience, the StubFinder interface extends StubTrigger, so you only need one +or the other in your tests.

    StubTrigger gives you the following options to trigger a message:

    93.1.1 Trigger by Label

    stubFinder.trigger('return_book_1')

    93.1.2 Trigger by Group and Artifact Ids

    stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1')

    93.1.3 Trigger by Artifact Ids

    stubFinder.trigger('streamService', 'return_book_1')

    93.1.4 Trigger All Messages

    stubFinder.trigger()

    93.2 Stub Runner Camel

    Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to integrate with Apache Camel. +For the provided artifacts it will automatically download the stubs and register the required +routes.

    93.2.1 Adding it to the project

    It’s enough to have both Apache Camel and Spring Cloud Contract Stub Runner on classpath. +Remember to annotate your test class with @AutoConfigureStubRunner.

    93.2.2 Disabling the functionality

    If you need to disable this functionality just pass stubrunner.camel.enabled=false property.

    93.2.3 Examples

    Stubs structure

    Let us assume that we have the following Maven repository with a deployed stubs for the +camelService application.

    └── .m2
    +    └── repository
    +        └── io
    +            └── codearte
    +                └── accurest
    +                    └── stubs
    +                        └── camelService
    +                            ├── 0.0.1-SNAPSHOT
    +                            │   ├── camelService-0.0.1-SNAPSHOT.pom
    +                            │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar
    +                            │   └── maven-metadata-local.xml
    +                            └── maven-metadata-local.xml

    And the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── repository
    +    ├── accurest
    +    │   ├── bookDeleted.groovy
    +    │   ├── bookReturned1.groovy
    +    │   └── bookReturned2.groovy
    +    └── mappings

    Let’s consider the following contracts (let' number it with 1):

    Contract.make {
    +	label 'return_book_1'
    +	input {
    +		triggeredBy('bookReturnedTriggered()')
    +	}
    +	outputMessage {
    +		sentTo('jms:output')
    +		body('''{ "bookName" : "foo" }''')
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    and number 2

    Contract.make {
    +	label 'return_book_2'
    +	input {
    +		messageFrom('jms:input')
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		messageHeaders {
    +			header('sample', 'header')
    +		}
    +	}
    +	outputMessage {
    +		sentTo('jms:output')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    Scenario 1 (no input message)

    So as to trigger a message via the return_book_1 label we’ll use the StubTigger interface as follows

    stubFinder.trigger('return_book_1')

    Next we’ll want to listen to the output of the message sent to jms:output

    Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)

    And the received message would pass the following assertions

    receivedMessage != null
    +assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
    +receivedMessage.in.headers.get('BOOK-NAME') == 'foo'

    Scenario 2 (output triggered by input)

    Since the route is set for you it’s enough to just send a message to the jms:output destination.

    producerTemplate.
    +		sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])

    Next we’ll want to listen to the output of the message sent to jms:output

    Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000)

    And the received message would pass the following assertions

    receivedMessage != null
    +assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
    +receivedMessage.in.headers.get('BOOK-NAME') == 'foo'

    Scenario 3 (input with no output)

    Since the route is set for you it’s enough to just send a message to the jms:output destination.

    producerTemplate.
    +		sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])

    93.3 Stub Runner Integration

    Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to +integrate with Spring Integration. For the provided artifacts, it automatically downloads +the stubs and registers the required routes.

    93.3.1 Adding the Runner to the Project

    You can have both Spring Integration and Spring Cloud Contract Stub Runner on the +classpath. Remember to annotate your test class with @AutoConfigureStubRunner.

    93.3.2 Disabling the functionality

    If you need to disable this functionality, set the +stubrunner.integration.enabled=false property.

    Assume that you have the following Maven repository with deployed stubs for the +integrationService application:

    └── .m2
    +    └── repository
    +        └── io
    +            └── codearte
    +                └── accurest
    +                    └── stubs
    +                        └── integrationService
    +                            ├── 0.0.1-SNAPSHOT
    +                            │   ├── integrationService-0.0.1-SNAPSHOT.pom
    +                            │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
    +                            │   └── maven-metadata-local.xml
    +                            └── maven-metadata-local.xml

    Further assume the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── repository
    +    ├── accurest
    +    │   ├── bookDeleted.groovy
    +    │   ├── bookReturned1.groovy
    +    │   └── bookReturned2.groovy
    +    └── mappings

    Consider the following contracts (numbered 1):

    Contract.make {
    +	label 'return_book_1'
    +	input {
    +		triggeredBy('bookReturnedTriggered()')
    +	}
    +	outputMessage {
    +		sentTo('output')
    +		body('''{ "bookName" : "foo" }''')
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    Now consider 2:

    Contract.make {
    +	label 'return_book_2'
    +	input {
    +		messageFrom('input')
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		messageHeaders {
    +			header('sample', 'header')
    +		}
    +	}
    +	outputMessage {
    +		sentTo('output')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    and the following Spring Integration Route:

    <?xml version="1.0" encoding="UTF-8"?>
    +<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +			 xmlns:beans="http://www.springframework.org/schema/beans"
    +			 xmlns="http://www.springframework.org/schema/integration"
    +			 xsi:schemaLocation="http://www.springframework.org/schema/beans
    +			https://www.springframework.org/schema/beans/spring-beans.xsd
    +			http://www.springframework.org/schema/integration
    +			http://www.springframework.org/schema/integration/spring-integration.xsd">
    +
    +
    +	<!-- REQUIRED FOR TESTING -->
    +	<bridge input-channel="output"
    +			output-channel="outputTest"/>
    +
    +	<channel id="outputTest">
    +		<queue/>
    +	</channel>
    +
    +</beans:beans>

    These examples lend themselves to three scenarios:

    Scenario 1 (no input message)

    To trigger a message via the return_book_1 label, use the StubTigger interface, as +follows:

    stubFinder.trigger('return_book_1')

    To listen to the output of the message sent to output:

    Message<?> receivedMessage = messaging.receive('outputTest')

    The received message would pass the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 2 (output triggered by input)

    Since the route is set for you, you can send a message to the output +destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'input')

    To listen to the output of the message sent to output:

    Message<?> receivedMessage = messaging.receive('outputTest')

    The received message passes the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 3 (input with no output)

    Since the route is set for you, you can send a message to the input destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

    93.4 Stub Runner Stream

    Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to +integrate with Spring Stream. For the provided artifacts, it automatically downloads the +stubs and registers the required routes.

    [Warning]Warning

    If Stub Runner’s integration with Stream the messageFrom or sentTo Strings +are resolved first as a destination of a channel and no such destination exists, the +destination is resolved as a channel name.

    [Important]Important

    If you want to use Spring Cloud Stream remember, to add a dependency on +org.springframework.cloud:spring-cloud-stream-test-support.

    Maven.  +

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-stream-test-support</artifactId>
    +    <scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile "org.springframework.cloud:spring-cloud-stream-test-support"

    +

    93.4.1 Adding the Runner to the Project

    You can have both Spring Cloud Stream and Spring Cloud Contract Stub Runner on the +classpath. Remember to annotate your test class with @AutoConfigureStubRunner.

    93.4.2 Disabling the functionality

    If you need to disable this functionality, set the stubrunner.stream.enabled=false +property.

    Assume that you have the following Maven repository with a deployed stubs for the +streamService application:

    └── .m2
    +    └── repository
    +        └── io
    +            └── codearte
    +                └── accurest
    +                    └── stubs
    +                        └── streamService
    +                            ├── 0.0.1-SNAPSHOT
    +                            │   ├── streamService-0.0.1-SNAPSHOT.pom
    +                            │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar
    +                            │   └── maven-metadata-local.xml
    +                            └── maven-metadata-local.xml

    Further assume the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── repository
    +    ├── accurest
    +    │   ├── bookDeleted.groovy
    +    │   ├── bookReturned1.groovy
    +    │   └── bookReturned2.groovy
    +    └── mappings

    Consider the following contracts (numbered 1):

    Contract.make {
    +	label 'return_book_1'
    +	input { triggeredBy('bookReturnedTriggered()') }
    +	outputMessage {
    +		sentTo('returnBook')
    +		body('''{ "bookName" : "foo" }''')
    +		headers { header('BOOK-NAME', 'foo') }
    +	}
    +}

    Now consider 2:

    Contract.make {
    +	label 'return_book_2'
    +	input {
    +		messageFrom('bookStorage')
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		messageHeaders { header('sample', 'header') }
    +	}
    +	outputMessage {
    +		sentTo('returnBook')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers { header('BOOK-NAME', 'foo') }
    +	}
    +}

    Now consider the following Spring configuration:

    stubrunner.repositoryRoot: classpath:m2repo/repository/
    +stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs
    +stubrunner.stubs-mode: remote
    +spring:
    +  cloud:
    +    stream:
    +      bindings:
    +        output:
    +          destination: returnBook
    +        input:
    +          destination: bookStorage
    +
    +server:
    +  port: 0
    +
    +debug: true

    These examples lend themselves to three scenarios:

    Scenario 1 (no input message)

    To trigger a message via the return_book_1 label, use the StubTrigger interface as +follows:

    stubFinder.trigger('return_book_1')

    To listen to the output of the message sent to a channel whose destination is +returnBook:

    Message<?> receivedMessage = messaging.receive('returnBook')

    The received message passes the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 2 (output triggered by input)

    Since the route is set for you, you can send a message to the bookStorage +destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage')

    To listen to the output of the message sent to returnBook:

    Message<?> receivedMessage = messaging.receive('returnBook')

    The received message passes the following assertions:

    receivedMessage != null
    +assertJsons(receivedMessage.payload)
    +receivedMessage.headers.get('BOOK-NAME') == 'foo'

    Scenario 3 (input with no output)

    Since the route is set for you, you can send a message to the output +destination:

    messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

    93.5 Stub Runner Spring AMQP

    Spring Cloud Contract Verifier Stub Runner’s messaging module provides an easy way to +integrate with Spring AMQP’s Rabbit Template. For the provided artifacts, it +automatically downloads the stubs and registers the required routes.

    The integration tries to work standalone (that is, without interaction with a running +RabbitMQ message broker). It expects a RabbitTemplate on the application context and +uses it as a spring boot test named @SpyBean. As a result, it can use the mockito spy +functionality to verify and inspect messages sent by the application.

    On the message consumer side, the stub runner considers all @RabbitListener annotated +endpoints and all SimpleMessageListenerContainer objects on the application context.

    As messages are usually sent to exchanges in AMQP, the message contract contains the +exchange name as the destination. Message listeners on the other side are bound to +queues. Bindings connect an exchange to a queue. If message contracts are triggered, the +Spring AMQP stub runner integration looks for bindings on the application context that +match this exchange. Then it collects the queues from the Spring exchanges and tries to +find message listeners bound to these queues. The message is triggered for all matching +message listeners.

    If you need to work with routing keys, it’s enough to pass them via the amqp_receivedRoutingKey +messaging header.

    93.5.1 Adding the Runner to the Project

    You can have both Spring AMQP and Spring Cloud Contract Stub Runner on the classpath and +set the property stubrunner.amqp.enabled=true. Remember to annotate your test class +with @AutoConfigureStubRunner.

    [Important]Important

    If you already have Stream and Integration on the classpath, you need +to disable them explicitly by setting the stubrunner.stream.enabled=false and +stubrunner.integration.enabled=false properties.

    Assume that you have the following Maven repository with a deployed stubs for the +spring-cloud-contract-amqp-test application.

    └── .m2
    +    └── repository
    +        └── com
    +            └── example
    +                └── spring-cloud-contract-amqp-test
    +                    ├── 0.4.0-SNAPSHOT
    +                    │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
    +                    │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
    +                    │   └── maven-metadata-local.xml
    +                    └── maven-metadata-local.xml

    Further assume that the stubs contain the following structure:

    ├── META-INF
    +│   └── MANIFEST.MF
    +└── contracts
    +    └── shouldProduceValidPersonData.groovy

    Consider the following contract:

    Contract.make {
    +	// Human readable description
    +	description 'Should produce valid person data'
    +	// Label by means of which the output message can be triggered
    +	label 'contract-test.person.created.event'
    +	// input to the contract
    +	input {
    +		// the contract will be triggered by a method
    +		triggeredBy('createPerson()')
    +	}
    +	// output message of the contract
    +	outputMessage {
    +		// destination to which the output message will be sent
    +		sentTo 'contract-test.exchange'
    +		headers {
    +			header('contentType': 'application/json')
    +			header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person')
    +		}
    +		// the body of the output message
    +		body([
    +				id  : $(consumer(9), producer(regex("[0-9]+"))),
    +				name: "me"
    +		])
    +	}
    +}

    Now consider the following Spring configuration:

    stubrunner:
    +  repositoryRoot: classpath:m2repo/repository/
    +  ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs
    +  stubs-mode: remote
    +  amqp:
    +    enabled: true
    +server:
    +  port: 0

    Triggering the message

    To trigger a message using the contract above, use the StubTrigger interface as +follows:

    stubTrigger.trigger("contract-test.person.created.event")

    The message has a destination of contract-test.exchange, so the Spring AMQP stub runner +integration looks for bindings related to this exchange.

    @Bean
    +public Binding binding() {
    +	return BindingBuilder.bind(new Queue("test.queue"))
    +			.to(new DirectExchange("contract-test.exchange")).with("#");
    +}

    The binding definition binds the queue test.queue. As a result, the following listener +definition is matched and invoked with the contract message.

    @Bean
    +public SimpleMessageListenerContainer simpleMessageListenerContainer(
    +		ConnectionFactory connectionFactory,
    +		MessageListenerAdapter listenerAdapter) {
    +	SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    +	container.setConnectionFactory(connectionFactory);
    +	container.setQueueNames("test.queue");
    +	container.setMessageListener(listenerAdapter);
    +
    +	return container;
    +}

    Also, the following annotated listener matches and is invoked:

    @RabbitListener(bindings = @QueueBinding(value = @Queue("test.queue"), exchange = @Exchange(value = "contract-test.exchange", ignoreDeclarationExceptions = "true")))
    +public void handlePerson(Person person) {
    +	this.person = person;
    +}
    [Note]Note

    The message is directly handed over to the onMessage method of the +MessageListener associated with the matching SimpleMessageListenerContainer.

    Spring AMQP Test Configuration

    In order to avoid Spring AMQP trying to connect to a running broker during our tests +configure a mock ConnectionFactory.

    To disable the mocked ConnectionFactory, set the following property: +stubrunner.amqp.mockConnection=false

    stubrunner:
    +  amqp:
    +    mockConnection: false

    94. Contract DSL

    Spring Cloud Contract supports out of the box 2 types of DSL. One written in +Groovy and one written in YAML.

    If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy +before. Knowledge of the language is not really needed, as the Contract DSL uses only a +tiny subset of it (only literals, method calls and closures). Also, the DSL is statically +typed, to make it programmer-readable without any knowledge of the DSL itself.

    [Important]Important

    Remember that, inside the Groovy contract file, you have to provide the fully +qualified name to the Contract class and make static imports, such as +org.springframework.cloud.spec.Contract.make { …​ }. You can also provide an import to +the Contract class: import org.springframework.cloud.spec.Contract and then call +Contract.make { …​ }.

    [Tip]Tip

    Spring Cloud Contract supports defining multiple contracts in a single file.

    The following is a complete example of a Groovy contract definition:

    The following is a complete example of a YAML contract definition:

    description: Some description
    +name: some name
    +priority: 8
    +ignored: true
    +request:
    +  url: /foo
    +  queryParameters:
    +    a: b
    +    b: c
    +  method: PUT
    +  headers:
    +    foo: bar
    +    fooReq: baz
    +  body:
    +    foo: bar
    +  matchers:
    +    body:
    +      - path: $.foo
    +        type: by_regex
    +        value: bar
    +    headers:
    +      - key: foo
    +        regex: bar
    +response:
    +  status: 200
    +  headers:
    +    foo2: bar
    +    foo3: foo33
    +    fooRes: baz
    +  body:
    +    foo2: bar
    +    foo3: baz
    +    nullValue: null
    +  matchers:
    +    body:
    +      - path: $.foo2
    +        type: by_regex
    +        value: bar
    +      - path: $.foo3
    +        type: by_command
    +        value: executeMe($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +    headers:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        command: andMeToo($it)
    [Tip]Tip

    You can compile contracts to stubs mapping using standalone maven command: +mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert

    94.1 Limitations

    [Warning]Warning

    Spring Cloud Contract Verifier does not properly support XML. Please use JSON or +help us implement this feature.

    [Warning]Warning

    The support for verifying the size of JSON arrays is experimental. If you want +to turn it on, please set the value of the following system property to true: +spring.cloud.contract.verifier.assert.size. By default, this feature is set to false. +You can also provide the assertJsonSize property in the plugin configuration.

    [Warning]Warning

    Because JSON structure can have any form, it can be impossible to parse it +properly when using the Groovy DSL and the value(consumer(…​), producer(…​)) notation in GString. That +is why you should use the Groovy Map notation.

    94.2 Common Top-Level elements

    The following sections describe the most common top-level elements:

    94.2.1 Description

    You can add a description to your contract. The description is arbitrary text. The +following code shows an example:

    Groovy DSL.  +

    			org.springframework.cloud.contract.spec.Contract.make {
    +				description('''
    +given:
    +	An input
    +when:
    +	Sth happens
    +then:
    +	Output
    +''')
    +			}

    +

    YAML.  +

    description: Some description
    +name: some name
    +priority: 8
    +ignored: true
    +request:
    +  url: /foo
    +  queryParameters:
    +    a: b
    +    b: c
    +  method: PUT
    +  headers:
    +    foo: bar
    +    fooReq: baz
    +  body:
    +    foo: bar
    +  matchers:
    +    body:
    +      - path: $.foo
    +        type: by_regex
    +        value: bar
    +    headers:
    +      - key: foo
    +        regex: bar
    +response:
    +  status: 200
    +  headers:
    +    foo2: bar
    +    foo3: foo33
    +    fooRes: baz
    +  body:
    +    foo2: bar
    +    foo3: baz
    +    nullValue: null
    +  matchers:
    +    body:
    +      - path: $.foo2
    +        type: by_regex
    +        value: bar
    +      - path: $.foo3
    +        type: by_command
    +        value: executeMe($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +    headers:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        command: andMeToo($it)

    +

    94.2.2 Name

    You can provide a name for your contract. Assume that you provided the following name: +should register a user. If you do so, the name of the autogenerated test is +validate_should_register_a_user. Also, the name of the stub in a WireMock stub is +should_register_a_user.json.

    [Important]Important

    You must ensure that the name does not contain any characters that make the +generated test not compile. Also, remember that, if you provide the same name for +multiple contracts, your autogenerated tests fail to compile and your generated stubs +override each other.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	name("some_special_name")
    +}

    +

    YAML.  +

    name: some name

    +

    94.2.3 Ignoring Contracts

    If you want to ignore a contract, you can either set a value of ignored contracts in the +plugin configuration or set the ignored property on the contract itself:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	ignored()
    +}

    +

    YAML.  +

    ignored: true

    +

    94.2.4 Passing Values from Files

    Starting with version 1.2.0, you can pass values from files. Assume that you have the +following resources in our project.

    └── src
    +    └── test
    +        └── resources
    +            └── contracts
    +                ├── readFromFile.groovy
    +                ├── request.json
    +                └── response.json

    Further assume that your contract is as follows:

    Groovy DSL.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +	request {
    +		method('PUT')
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(file("request.json"))
    +		url("/1")
    +	}
    +	response {
    +		status OK()
    +		body(file("response.json"))
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +}

    +

    YAML.  +

    request:
    +  method: GET
    +  url: /foo
    +  bodyFromFile: request.json
    +response:
    +  status: 200
    +  bodyFromFile: response.json

    +

    Further assume that the JSON files is as follows:

    request.json

    {
    +  "status": "REQUEST"
    +}

    response.json

    {
    +  "status": "RESPONSE"
    +}

    When test or stub generation takes place, the contents of the file is passed to the body +of a request or a response. The name of the file needs to be a file with location +relative to the folder in which the contract lays.

    If you need to pass the contents of a file in a binary form +it’s enough for you to use the fileAsBytes method in Groovy DSL or bodyFromFileAsBytes field in YAML.

    Groovy DSL.  +

    import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +	request {
    +		url("/1")
    +		method(PUT())
    +		headers {
    +			contentType(applicationOctetStream())
    +		}
    +		body(fileAsBytes("request.pdf"))
    +	}
    +	response {
    +		status 200
    +		body(fileAsBytes("response.pdf"))
    +		headers {
    +			contentType(applicationOctetStream())
    +		}
    +	}
    +}

    +

    YAML.  +

    request:
    +  url: /1
    +  method: PUT
    +  headers:
    +    Content-Type: application/octet-stream
    +  bodyFromFileAsBytes: request.pdf
    +response:
    +  status: 200
    +  bodyFromFileAsBytes: response.pdf
    +  headers:
    +    Content-Type: application/octet-stream

    +

    [Important]Important

    You should use this approach whenever you want to work with binary payloads both for HTTP and messaging.

    94.2.5 HTTP Top-Level Elements

    The following methods can be called in the top-level closure of a contract definition. +request and response are mandatory. priority is optional.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	// Definition of HTTP request part of the contract
    +	// (this can be a valid request or invalid depending
    +	// on type of contract being specified).
    +	request {
    +		method GET()
    +		url "/foo"
    +		//...
    +	}
    +
    +	// Definition of HTTP response part of the contract
    +	// (a service implementing this contract should respond
    +	// with following response after receiving request
    +	// specified in "request" part above).
    +	response {
    +		status 200
    +		//...
    +	}
    +
    +	// Contract priority, which can be used for overriding
    +	// contracts (1 is highest). Priority is optional.
    +	priority 1
    +}

    +

    YAML.  +

    priority: 8
    +request:
    +...
    +response:
    +...

    +

    [Important]Important

    If you want to make your contract have a higher value of priority +you need to pass a lower number to the priority tag / method. E.g. priority with +value 5 has higher priority than priority with value 10.

    94.3 Request

    The HTTP protocol requires only method and url to be specified in a request. The +same information is mandatory in request definition of the Contract.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		// HTTP request method (GET/POST/PUT/DELETE).
    +		method 'GET'
    +
    +		// Path component of request URL is specified as follows.
    +		urlPath('/users')
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    method: PUT
    +url: /foo

    +

    It is possible to specify an absolute rather than relative url, but using urlPath is +the recommended way, as doing so makes the tests host-independent.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'GET'
    +
    +		// Specifying `url` and `urlPath` in one contract is illegal.
    +		url('http://localhost:8888/users')
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +  method: PUT
    +  urlPath: /foo

    +

    request may contain query parameters.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +
    +		urlPath('/users') {
    +
    +			// Each parameter is specified in form
    +			// `'paramName' : paramValue` where parameter value
    +			// may be a simple literal or one of matcher functions,
    +			// all of which are used in this example.
    +			queryParameters {
    +
    +				// If a simple literal is used as value
    +				// default matcher function is used (equalTo)
    +				parameter 'limit': 100
    +
    +				// `equalTo` function simply compares passed value
    +				// using identity operator (==).
    +				parameter 'filter': equalTo("email")
    +
    +				// `containing` function matches strings
    +				// that contains passed substring.
    +				parameter 'gender': value(consumer(containing("[mf]")), producer('mf'))
    +
    +				// `matching` function tests parameter
    +				// against passed regular expression.
    +				parameter 'offset': value(consumer(matching("[0-9]+")), producer(123))
    +
    +				// `notMatching` functions tests if parameter
    +				// does not match passed regular expression.
    +				parameter 'loginStartsWith': value(consumer(notMatching(".{0,2}")), producer(3))
    +			}
    +		}
    +
    +		//...
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +  queryParameters:
    +    a: b
    +    b: c
    +  headers:
    +    foo: bar
    +    fooReq: baz
    +  cookies:
    +    foo: bar
    +    fooReq: baz
    +  body:
    +    foo: bar
    +  matchers:
    +    body:
    +      - path: $.foo
    +        type: by_regex
    +        value: bar
    +    headers:
    +      - key: foo
    +        regex: bar
    +response:
    +  status: 200
    +  fixedDelayMilliseconds: 1000
    +  headers:
    +    foo2: bar
    +    foo3: foo33
    +    fooRes: baz
    +  body:
    +    foo2: bar
    +    foo3: baz
    +    nullValue: null
    +  matchers:
    +    body:
    +      - path: $.foo2
    +        type: by_regex
    +        value: bar
    +      - path: $.foo3
    +        type: by_command
    +        value: executeMe($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +    headers:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        command: andMeToo($it)
    +    cookies:
    +      - key: foo2
    +        regex: bar
    +      - key: foo3
    +        predefined:

    +

    request may contain additional request headers, as shown in the following example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +
    +		// Each header is added in form `'Header-Name' : 'Header-Value'`.
    +		// there are also some helper methods
    +		headers {
    +			header 'key': 'value'
    +			contentType(applicationJson())
    +		}
    +
    +		//...
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +headers:
    +  foo: bar
    +  fooReq: baz

    +

    request may contain additional request cookies, as shown in the following example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +
    +		// Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`.
    +		// there are also some helper methods
    +		cookies {
    +			cookie 'key': 'value'
    +			cookie('another_key', 'another_value')
    +		}
    +
    +		//...
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +cookies:
    +  foo: bar
    +  fooReq: baz

    +

    request may contain a request body:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +
    +		// Currently only JSON format of request body is supported.
    +		// Format will be determined from a header or body's content.
    +		body '''{ "login" : "john", "name": "John The Contract" }'''
    +	}
    +
    +	response {
    +		//...
    +		status 200
    +	}
    +}

    +

    YAML.  +

    request:
    +...
    +body:
    +  foo: bar

    +

    request may contain multipart elements. To include multipart elements, use the +multipart method/section, as shown in the following examples

    Groovy DSL.  +

    +

    YAML.  +

    request:
    +  method: PUT
    +  url: /multipart
    +  headers:
    +    Content-Type: multipart/form-data;boundary=AaB03x
    +  multipart:
    +    params:
    +      # key (parameter name), value (parameter value) pair
    +      formParameter: '"formParameterValue"'
    +      someBooleanParameter: true
    +    named:
    +      - paramName: file
    +        fileName: filename.csv
    +        fileContent: file content
    +  matchers:
    +    multipart:
    +      params:
    +        - key: formParameter
    +          regex: ".+"
    +        - key: someBooleanParameter
    +          predefined: any_boolean
    +      named:
    +        - paramName: file
    +          fileName:
    +            predefined: non_empty
    +          fileContent:
    +            predefined: non_empty
    +response:
    +  status: 200

    +

    In the preceding example, we define parameters in either of two ways:

    Groovy DSL

    • Directly, by using the map notation, where the value can be a dynamic property (such as +formParameter: $(consumer(…​), producer(…​))).
    • By using the named(…​) method that lets you set a named parameter. A named parameter +can set a name and content. You can call it either via a method with two arguments, +such as named("fileName", "fileContent"), or via a map notation, such as +named(name: "fileName", content: "fileContent").

    YAML

    • The multipart parameters are set via multipart.params section
    • The named parameters (the fileName and fileContent for a given parameter name) +can be set via the multipart.named section. That section contains +the paramName (name of the parameter), fileName (name of the file), +fileContent (content of the file) fields
    • The dynamic bits can be set via the matchers.multipart section

      • for parameters use the params section that can accept +regex or a predefined regular expression
      • for named params use the named section where first you +define the parameter name via paramName and then you can pass the +parametrization of either fileName or fileContent via +regex or a predefined regular expression

    From this contract, the generated test is as follows:

    // given:
    + MockMvcRequestSpecification request = given()
    +   .header("Content-Type", "multipart/form-data;boundary=AaB03x")
    +   .param("formParameter", "\"formParameterValue\"")
    +   .param("someBooleanParameter", "true")
    +   .multiPart("file", "filename.csv", "file content".getBytes());
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .put("/multipart");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);

    The WireMock stub is as follows:

    			'''
    +{
    +  "request" : {
    +	"url" : "/multipart",
    +	"method" : "PUT",
    +	"headers" : {
    +	  "Content-Type" : {
    +		"matches" : "multipart/form-data;boundary=AaB03x.*"
    +	  }
    +	},
    +	"bodyPatterns" : [ {
    +		"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n\\".+\\"\\r\\n--\\\\1.*"
    +  		}, {
    +    			"matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n(true|false)\\r\\n--\\\\1.*"
    +  		}, {
    +	  "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n[\\\\S\\\\s]+\\r\\n--\\\\1.*"
    +	} ]
    +  },
    +  "response" : {
    +	"status" : 200,
    +	"transformers" : [ "response-template", "foo-transformer" ]
    +  }
    +}
    +	'''

    94.4 Response

    The response must contain an HTTP status code and may contain other information. The +following code shows an example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		//...
    +		method GET()
    +		url "/foo"
    +	}
    +	response {
    +		// Status code sent by the server
    +		// in response to request specified above.
    +		status OK()
    +	}
    +}

    +

    YAML.  +

    response:
    +...
    +status: 200

    +

    Besides status, the response may contain headers, cookies and a body, both of which are +specified the same way as in the request (see the previous paragraph).

    [Tip]Tip

    Via the Groovy DSL you can reference the org.springframework.cloud.contract.spec.internal.HttpStatus +methods to provide a meaningful status instead of a digit. E.g. you can call +OK() for a status 200 or BAD_REQUEST() for 400.

    94.5 Dynamic properties

    The contract can contain some dynamic properties: timestamps, IDs, and so on. You do not +want to force the consumers to stub their clocks to always return the same value of time +so that it gets matched by the stub.

    For Groovy DSL you can provide the dynamic parts in your contracts +in two ways: pass them directly in the body or set them in a separate section called +bodyMatchers.

    [Note]Note

    Before 2.0.0 these were set using testMatchers and stubMatchers, +check out the migration guide for more information.

    For YAML you can only use the matchers section.

    94.5.1 Dynamic properties inside the body

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    You can set the properties inside the body either with the value method or, if you use +the Groovy map notation, with $(). The following example shows how to set dynamic +properties with the value method:

    value(consumer(...), producer(...))
    +value(c(...), p(...))
    +value(stub(...), test(...))
    +value(client(...), server(...))

    The following example shows how to set dynamic properties with $():

    $(consumer(...), producer(...))
    +$(c(...), p(...))
    +$(stub(...), test(...))
    +$(client(...), server(...))

    Both approaches work equally well. stub and client methods are aliases over the consumer +method. Subsequent sections take a closer look at what you can do with those values.

    94.5.2 Regular expressions

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    You can use regular expressions to write your requests in Contract DSL. Doing so is +particularly useful when you want to indicate that a given response should be provided +for requests that follow a given pattern. Also, you can use regular expressions when you +need to use patterns and not exact values both for your test and your server side tests.

    The following example shows how to use regular expressions to write a request:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method('GET')
    +		url $(consumer(~/\/[0-9]{2}/), producer('/12'))
    +	}
    +	response {
    +		status OK()
    +		body(
    +				id: $(anyNumber()),
    +				surname: $(
    +						consumer('Kowalsky'),
    +						producer(regex('[a-zA-Z]+'))
    +				),
    +				name: 'Jan',
    +				created: $(consumer('2014-02-02 12:23:43'), producer(execute('currentDate(it)'))),
    +				correlationId: value(consumer('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'),
    +						producer(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'))
    +				)
    +		)
    +		headers {
    +			header 'Content-Type': 'text/plain'
    +		}
    +	}
    +}

    You can also provide only one side of the communication with a regular expression. If you +do so, then the contract engine automatically provides the generated string that matches +the provided regular expression. The following code shows an example:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'PUT'
    +		url value(consumer(regex('/foo/[0-9]{5}')))
    +		body([
    +				requestElement: $(consumer(regex('[0-9]{5}')))
    +		])
    +		headers {
    +			header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*'))))
    +		}
    +	}
    +	response {
    +		status OK()
    +		body([
    +				responseElement: $(producer(regex('[0-9]{7}')))
    +		])
    +		headers {
    +			contentType("application/vnd.fraud.v1+json")
    +		}
    +	}
    +}

    In the preceding example, the opposite side of the communication has the respective data +generated for request and response.

    Spring Cloud Contract comes with a series of predefined regular expressions that you can +use in your contracts, as shown in the following example:

    protected static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/)
    +protected static final Pattern ALPHA_NUMERIC = Pattern.compile('[a-zA-Z0-9]+')
    +protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/)
    +protected static final Pattern NUMBER = Pattern.compile('-?(\\d*\\.\\d+|\\d+)')
    +protected static final Pattern INTEGER = Pattern.compile('-?(\\d+)')
    +protected static final Pattern POSITIVE_INT = Pattern.compile('([1-9]\\d*)')
    +protected static final Pattern DOUBLE = Pattern.compile('-?(\\d*\\.\\d+)')
    +protected static final Pattern HEX = Pattern.compile('[a-fA-F0-9]+')
    +protected static final Pattern IP_ADDRESS = Pattern.
    +		compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])')
    +protected static final Pattern HOSTNAME_PATTERN = Pattern.
    +		compile('((http[s]?|ftp):/)/?([^:/\\s]+)(:[0-9]{1,5})?')
    +protected static final Pattern EMAIL = Pattern.
    +		compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}')
    +protected static final Pattern URL = UrlHelper.URL
    +protected static final Pattern HTTPS_URL = UrlHelper.HTTPS_URL
    +protected static final Pattern UUID = Pattern.
    +		compile('[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}')
    +protected static final Pattern ANY_DATE = Pattern.
    +		compile('(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])')
    +protected static final Pattern ANY_DATE_TIME = Pattern.
    +		compile('([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])')
    +protected static final Pattern ANY_TIME = Pattern.
    +		compile('(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])')
    +protected static final Pattern NON_EMPTY = Pattern.compile(/[\S\s]+/)
    +protected static final Pattern NON_BLANK = Pattern.compile(/^\s*\S[\S\s]*/)
    +protected static final Pattern ISO8601_WITH_OFFSET = Pattern.
    +		compile(/([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.\d{1,6})?(Z|[+-][01]\d:[0-5]\d)/)
    +
    +protected static Pattern anyOf(String... values) {
    +	return Pattern.compile(values.collect({ "^$it\$" }).join("|"))
    +}
    +
    +RegexProperty onlyAlphaUnicode() {
    +	return new RegexProperty(ONLY_ALPHA_UNICODE).asString()
    +}
    +
    +RegexProperty alphaNumeric() {
    +	return new RegexProperty(ALPHA_NUMERIC).asString()
    +}
    +
    +RegexProperty number() {
    +	return new RegexProperty(NUMBER).asDouble()
    +}
    +
    +RegexProperty positiveInt() {
    +	return new RegexProperty(POSITIVE_INT).asInteger()
    +}
    +
    +RegexProperty anyBoolean() {
    +	return new RegexProperty(TRUE_OR_FALSE).asBooleanType()
    +}
    +
    +RegexProperty anInteger() {
    +	return new RegexProperty(INTEGER).asInteger()
    +}
    +
    +RegexProperty aDouble() {
    +	return new RegexProperty(DOUBLE).asDouble()
    +}
    +
    +RegexProperty ipAddress() {
    +	return new RegexProperty(IP_ADDRESS).asString()
    +}
    +
    +RegexProperty hostname() {
    +	return new RegexProperty(HOSTNAME_PATTERN).asString()
    +}
    +
    +RegexProperty email() {
    +	return new RegexProperty(EMAIL).asString()
    +}
    +
    +RegexProperty url() {
    +	return new RegexProperty(URL).asString()
    +}
    +
    +RegexProperty httpsUrl() {
    +	return new RegexProperty(HTTPS_URL).asString()
    +}
    +
    +RegexProperty uuid() {
    +	return new RegexProperty(UUID).asString()
    +}
    +
    +RegexProperty isoDate() {
    +	return new RegexProperty(ANY_DATE).asString()
    +}
    +
    +RegexProperty isoDateTime() {
    +	return new RegexProperty(ANY_DATE_TIME).asString()
    +}
    +
    +RegexProperty isoTime() {
    +	return new RegexProperty(ANY_TIME).asString()
    +}
    +
    +RegexProperty iso8601WithOffset() {
    +	return new RegexProperty(ISO8601_WITH_OFFSET).asString()
    +}
    +
    +RegexProperty nonEmpty() {
    +	return new RegexProperty(NON_EMPTY).asString()
    +}
    +
    +RegexProperty nonBlank() {
    +	return new RegexProperty(NON_BLANK).asString()
    +}

    In your contract, you can use it as shown in the following example:

    Contract dslWithOptionalsInString = Contract.make {
    +	priority 1
    +	request {
    +		method POST()
    +		url '/users/password'
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(
    +				email: $(consumer(optional(regex(email()))), producer('abc@abc.com')),
    +				callback_url: $(consumer(regex(hostname())), producer('http://partners.com'))
    +		)
    +	}
    +	response {
    +		status 404
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(
    +				code: value(consumer("123123"), producer(optional("123123"))),
    +				message: "User not found by email = [${value(producer(regex(email())), consumer('not.existing@user.com'))}]"
    +		)
    +	}
    +}

    To make matters even simpler you can use a set of predefined objects that will automatically assume that you want a regular expression to be passed. +All of those methods start with any prefix:

    T anyAlphaUnicode()
    +
    +T anyAlphaNumeric()
    +
    +T anyNumber()
    +
    +T anyInteger()
    +
    +T anyPositiveInt()
    +
    +T anyDouble()
    +
    +T anyHex()
    +
    +T aBoolean()
    +
    +T anyIpAddress()
    +
    +T anyHostname()
    +
    +T anyEmail()
    +
    +T anyUrl()
    +
    +T anyHttpsUrl()
    +
    +T anyUuid()
    +
    +T anyDate()
    +
    +T anyDateTime()
    +
    +T anyTime()
    +
    +T anyIso8601WithOffset()
    +
    +T anyNonBlankString()
    +
    +T anyNonEmptyString()
    +
    +T anyOf(String... values)

    and this is an example of how you can reference those methods:

    Contract contractDsl = Contract.make {
    +	label 'trigger_event'
    +	input {
    +		triggeredBy('toString()')
    +	}
    +	outputMessage {
    +		sentTo 'topic.rateablequote'
    +		body([
    +				alpha            : $(anyAlphaUnicode()),
    +				number           : $(anyNumber()),
    +				anInteger        : $(anyInteger()),
    +				positiveInt      : $(anyPositiveInt()),
    +				aDouble          : $(anyDouble()),
    +				aBoolean         : $(aBoolean()),
    +				ip               : $(anyIpAddress()),
    +				hostname         : $(anyHostname()),
    +				email            : $(anyEmail()),
    +				url              : $(anyUrl()),
    +				httpsUrl         : $(anyHttpsUrl()),
    +				uuid             : $(anyUuid()),
    +				date             : $(anyDate()),
    +				dateTime         : $(anyDateTime()),
    +				time             : $(anyTime()),
    +				iso8601WithOffset: $(anyIso8601WithOffset()),
    +				nonBlankString   : $(anyNonBlankString()),
    +				nonEmptyString   : $(anyNonEmptyString()),
    +				anyOf            : $(anyOf('foo', 'bar'))
    +		])
    +	}
    +}

    94.5.3 Passing Optional Parameters

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    It is possible to provide optional parameters in your contract. However, you can provide +optional parameters only for the following:

    • STUB side of the Request
    • TEST side of the Response

    The following example shows how to provide optional parameters:

    org.springframework.cloud.contract.spec.Contract.make {
    +	priority 1
    +	request {
    +		method 'POST'
    +		url '/users/password'
    +		headers {
    +			contentType(applicationJson())
    +		}
    +		body(
    +				email: $(consumer(optional(regex(email()))), producer('abc@abc.com')),
    +				callback_url: $(consumer(regex(hostname())), producer('https://partners.com'))
    +		)
    +	}
    +	response {
    +		status 404
    +		headers {
    +			header 'Content-Type': 'application/json'
    +		}
    +		body(
    +				code: value(consumer("123123"), producer(optional("123123")))
    +		)
    +	}
    +}

    By wrapping a part of the body with the optional() method, you create a regular +expression that must be present 0 or more times.

    If you use Spock for, the following test would be generated from the previous example:

    					"""
    + given:
    +  def request = given()
    +    .header("Content-Type", "application/json")
    +    .body('''{"email":"abc@abc.com","callback_url":"https://partners.com"}''')
    +
    + when:
    +  def response = given().spec(request)
    +    .post("/users/password")
    +
    + then:
    +  response.statusCode == 404
    +  response.header('Content-Type')  == 'application/json'
    + and:
    +  DocumentContext parsedJson = JsonPath.parse(response.body.asString())
    +  assertThatJson(parsedJson).field("['code']").matches("(123123)?")
    +"""

    The following stub would also be generated:

    					'''
    +{
    +  "request" : {
    +	"url" : "/users/password",
    +	"method" : "POST",
    +	"bodyPatterns" : [ {
    +	  "matchesJsonPath" : "$[?(@.['email'] =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6})?/)]"
    +	}, {
    +	  "matchesJsonPath" : "$[?(@.['callback_url'] =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]"
    +	} ],
    +	"headers" : {
    +	  "Content-Type" : {
    +		"equalTo" : "application/json"
    +	  }
    +	}
    +  },
    +  "response" : {
    +	"status" : 404,
    +	"body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}",
    +	"headers" : {
    +	  "Content-Type" : "application/json"
    +	}
    +  },
    +  "priority" : 1
    +}
    +'''

    94.5.4 Executing Custom Methods on the Server Side

    [Important]Important

    This section is valid only for Groovy DSL. Check out the +Section 94.5.7, “Dynamic Properties in the Matchers Sections” section for YAML examples of a similar feature.

    You can define a method call that executes on the server side during the test. Such a +method can be added to the class defined as "baseClassForTests" in the configuration. The +following code shows an example of the contract portion of the test case:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'PUT'
    +		url $(consumer(regex('^/api/[0-9]{2}$')), producer('/api/12'))
    +		headers {
    +			header 'Content-Type': 'application/json'
    +		}
    +		body '''\
    +			[{
    +				"text": "Gonna see you at Warsaw"
    +			}]
    +		'''
    +	}
    +	response {
    +		body(
    +				path: $(consumer('/api/12'), producer(regex('^/api/[0-9]{2}$'))),
    +				correlationId: $(consumer('1223456'), producer(execute('isProperCorrelationId($it)')))
    +		)
    +		status OK()
    +	}
    +}

    The following code shows the base class portion of the test case:

    abstract class BaseMockMvcSpec extends Specification {
    +
    +	def setup() {
    +		RestAssuredMockMvc.standaloneSetup(new PairIdController())
    +	}
    +
    +	void isProperCorrelationId(Integer correlationId) {
    +		assert correlationId == 123456
    +	}
    +
    +	void isEmpty(String value) {
    +		assert value == null
    +	}
    +
    +}
    [Important]Important

    You cannot use both a String and execute to perform concatenation. For +example, calling header('Authorization', 'Bearer ' + execute('authToken()')) leads to +improper results. Instead, call header('Authorization', execute('authToken()')) and +ensure that the authToken() method returns everything you need.

    The type of the object read from the JSON can be one of the following, depending on the +JSON path:

    • String: If you point to a String value in the JSON.
    • JSONArray: If you point to a List in the JSON.
    • Map: If you point to a Map in the JSON.
    • Number: If you point to Integer, Double etc. in the JSON.
    • Boolean: If you point to a Boolean in the JSON.

    In the request part of the contract, you can specify that the body should be taken from +a method.

    [Important]Important

    You must provide both the consumer and the producer side. The execute part +is applied for the whole body - not for parts of it.

    The following example shows how to read an object from JSON:

    Contract contractDsl = Contract.make {
    +	request {
    +		method 'GET'
    +		url '/something'
    +		body(
    +				$(c('foo'), p(execute('hashCode()')))
    +		)
    +	}
    +	response {
    +		status OK()
    +	}
    +}

    The preceding example results in calling the hashCode() method in the request body. +It should resemble the following code:

    // given:
    + MockMvcRequestSpecification request = given()
    +   .body(hashCode());
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .get("/something");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);

    94.5.5 Referencing the Request from the Response

    The best situation is to provide fixed values, but sometimes you need to reference a +request in your response.

    If you’re writing contracts using Groovy DSL, you can use the fromRequest() method, which lets +you reference a bunch of elements from the HTTP request. You can use the following +options:

    • fromRequest().url(): Returns the request URL and query parameters.
    • fromRequest().query(String key): Returns the first query parameter with a given name.
    • fromRequest().query(String key, int index): Returns the nth query parameter with a +given name.
    • fromRequest().path(): Returns the full path.
    • fromRequest().path(int index): Returns the nth path element.
    • fromRequest().header(String key): Returns the first header with a given name.
    • fromRequest().header(String key, int index): Returns the nth header with a given name.
    • fromRequest().body(): Returns the full request body.
    • fromRequest().body(String jsonPath): Returns the element from the request that +matches the JSON Path.

    If you’re using the YAML contract definition you have to use the +Handlebars {{{ }}} notation with custom, Spring Cloud Contract + functions to achieve this.

    • {{{ request.url }}}: Returns the request URL and query parameters.
    • {{{ request.query.key.[index] }}}: Returns the nth query parameter with a given name. +E.g. for key foo, first entry {{{ request.query.foo.[0] }}}
    • {{{ request.path }}}: Returns the full path.
    • {{{ request.path.[index] }}}: Returns the nth path element. E.g. +for first entry `{{{ request.path.[0] }}}
    • {{{ request.headers.key }}}: Returns the first header with a given name.
    • {{{ request.headers.key.[index] }}}: Returns the nth header with a given name.
    • {{{ request.body }}}: Returns the full request body.
    • {{{ jsonpath this 'your.json.path' }}}: Returns the element from the request that +matches the JSON Path. E.g. for json path $.foo - {{{ jsonpath this '$.foo' }}}

    Consider the following contract:

    Groovy DSL.  +

    +

    YAML.  +

    request:
    +  method: GET
    +  url: /api/v1/xxxx
    +  queryParameters:
    +    foo:
    +      - bar
    +      - bar2
    +  headers:
    +    Authorization:
    +      - secret
    +      - secret2
    +  body:
    +    foo: bar
    +    baz: 5
    +response:
    +  status: 200
    +  headers:
    +    Authorization: "foo {{{ request.headers.Authorization.0 }}} bar"
    +  body:
    +    url: "{{{ request.url }}}"
    +    path: "{{{ request.path }}}"
    +    pathIndex: "{{{ request.path.1 }}}"
    +    param: "{{{ request.query.foo }}}"
    +    paramIndex: "{{{ request.query.foo.1 }}}"
    +    authorization: "{{{ request.headers.Authorization.0 }}}"
    +    authorization2: "{{{ request.headers.Authorization.1 }}"
    +    fullBody: "{{{ request.body }}}"
    +    responseFoo: "{{{ jsonpath this '$.foo' }}}"
    +    responseBaz: "{{{ jsonpath this '$.baz' }}}"
    +    responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla"

    +

    Running a JUnit test generation leads to a test that resembles the following example:

    // given:
    + MockMvcRequestSpecification request = given()
    +   .header("Authorization", "secret")
    +   .header("Authorization", "secret2")
    +   .body("{\"foo\":\"bar\",\"baz\":5}");
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .queryParam("foo","bar")
    +   .queryParam("foo","bar2")
    +   .get("/api/v1/xxxx");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);
    + assertThat(response.header("Authorization")).isEqualTo("foo secret bar");
    +// and:
    + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    + assertThatJson(parsedJson).field("['fullBody']").isEqualTo("{\"foo\":\"bar\",\"baz\":5}");
    + assertThatJson(parsedJson).field("['authorization']").isEqualTo("secret");
    + assertThatJson(parsedJson).field("['authorization2']").isEqualTo("secret2");
    + assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx");
    + assertThatJson(parsedJson).field("['param']").isEqualTo("bar");
    + assertThatJson(parsedJson).field("['paramIndex']").isEqualTo("bar2");
    + assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1");
    + assertThatJson(parsedJson).field("['responseBaz']").isEqualTo(5);
    + assertThatJson(parsedJson).field("['responseFoo']").isEqualTo("bar");
    + assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2");
    + assertThatJson(parsedJson).field("['responseBaz2']").isEqualTo("Bla bla bar bla bla");

    As you can see, elements from the request have been properly referenced in the response.

    The generated WireMock stub should resemble the following example:

    {
    +  "request" : {
    +    "urlPath" : "/api/v1/xxxx",
    +    "method" : "POST",
    +    "headers" : {
    +      "Authorization" : {
    +        "equalTo" : "secret2"
    +      }
    +    },
    +    "queryParameters" : {
    +      "foo" : {
    +        "equalTo" : "bar2"
    +      }
    +    },
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$[?(@.['baz'] == 5)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.['foo'] == 'bar')]"
    +    } ]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "{\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"path\":\"{{{request.path}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"param\":\"{{{request.query.foo.[0]}}}\",\"pathIndex\":\"{{{request.path.[1]}}}\",\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"url\":\"{{{request.url}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\"}",
    +    "headers" : {
    +      "Authorization" : "{{{request.headers.Authorization.[0]}}};foo"
    +    },
    +    "transformers" : [ "response-template" ]
    +  }
    +}

    Sending a request such as the one presented in the request part of the contract results +in sending the following response body:

    {
    +  "url" : "/api/v1/xxxx?foo=bar&foo=bar2",
    +  "path" : "/api/v1/xxxx",
    +  "pathIndex" : "v1",
    +  "param" : "bar",
    +  "paramIndex" : "bar2",
    +  "authorization" : "secret",
    +  "authorization2" : "secret2",
    +  "fullBody" : "{\"foo\":\"bar\",\"baz\":5}",
    +  "responseFoo" : "bar",
    +  "responseBaz" : 5,
    +  "responseBaz2" : "Bla bla bar bla bla"
    +}
    [Important]Important

    This feature works only with WireMock having a version greater than or equal +to 2.5.1. The Spring Cloud Contract Verifier uses WireMock’s +response-template response transformer. It uses Handlebars to convert the Mustache {{{ }}} templates into +proper values. Additionally, it registers two helper functions:

    • escapejsonbody: Escapes the request body in a format that can be embedded in a JSON.
    • jsonpath: For a given parameter, find an object in the request body.

    94.5.6 Registering Your Own WireMock Extension

    WireMock lets you register custom extensions. By default, Spring Cloud Contract registers +the transformer, which lets you reference a request from a response. If you want to +provide your own extensions, you can register an implementation of the +org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions interface. +Since we use the spring.factories extension approach, you can create an entry in +META-INF/spring.factories file similar to the following:

    org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions=\
    +org.springframework.cloud.contract.stubrunner.provider.wiremock.TestWireMockExtensions
    +org.springframework.cloud.contract.spec.ContractConverter=\
    +org.springframework.cloud.contract.stubrunner.TestCustomYamlContractConverter

    The following is an example of a custom extension:

    TestWireMockExtensions.groovy.  +

    /*
    + * Copyright 2013-2019 the original author or authors.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      https://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package org.springframework.cloud.contract.verifier.dsl.wiremock
    +
    +import com.github.tomakehurst.wiremock.extension.Extension
    +
    +/**
    + * Extension that registers the default transformer and the custom one
    + */
    +class TestWireMockExtensions implements WireMockExtensions {
    +	@Override
    +	List<Extension> extensions() {
    +		return [
    +				new DefaultResponseTransformer(),
    +				new CustomExtension()
    +		]
    +	}
    +}
    +
    +class CustomExtension implements Extension {
    +
    +	@Override
    +	String getName() {
    +		return "foo-transformer"
    +	}
    +}

    +

    [Important]Important

    Remember to override the applyGlobally() method and set it to false if you +want the transformation to be applied only for a mapping that explicitly requires it.

    94.5.7 Dynamic Properties in the Matchers Sections

    If you work with Pact, the following discussion may seem familiar. +Quite a few users are used to having a separation between the body and setting the +dynamic parts of a contract.

    You can use the bodyMatchers section for two reasons:

    • Define the dynamic values that should end up in a stub. +You can set it in the request or inputMessage part of your contract.
    • Verify the result of your test. +This section is present in the response or outputMessage side of the +contract.

    Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the +following matching possibilities:

    Groovy DSL

    • For the stubs(in tests on the Consumer’s side):

      • byEquality(): The value taken from the consumer’s request via the provided JSON Path must be +equal to the value provided in the contract.
      • byRegex(…​): The value taken from the consumer’s request via the provided JSON Path must +match the regex. You can also pass the type of the expected matched value (e.g. asString(), asLong() etc.)
      • byDate(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO Date value.
      • byTimestamp(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO DateTime value.
      • byTime(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO Time value.
    • For the verification(in generated tests on the Producer’s side):

      • byEquality(): The value taken from the producer’s response via the provided JSON Path must be +equal to the provided value in the contract.
      • byRegex(…​): The value taken from the producer’s response via the provided JSON Path must +match the regex.
      • byDate(): The value taken from the producer’s response via the provided JSON Path must match +the regex for an ISO Date value.
      • byTimestamp(): The value taken from the producer’s response via the provided JSON Path must +match the regex for an ISO DateTime value.
      • byTime(): The value taken from the producer’s response via the provided JSON Path must match +the regex for an ISO Time value.
      • byType(): The value taken from the producer’s response via the provided JSON Path needs to be +of the same type as the type defined in the body of the response in the contract. +byType can take a closure, in which you can set minOccurrence and maxOccurrence. For the request side, you should use the closure to assert size of the collection. +That way, you can assert the size of the flattened collection. To check the size of an +unflattened collection, use a custom method with the byCommand(…​) testMatcher.
      • byCommand(…​): The value taken from the producer’s response via the provided JSON Path is +passed as an input to the custom method that you provide. For example, +byCommand('foo($it)') results in calling a foo method to which the value matching the +JSON Path gets passed. The type of the object read from the JSON can be one of the +following, depending on the JSON path:

        • String: If you point to a String value.
        • JSONArray: If you point to a List.
        • Map: If you point to a Map.
        • Number: If you point to Integer, Double, or other kind of number.
        • Boolean: If you point to a Boolean.
      • byNull(): The value taken from the response via the provided JSON Path must be null

    YAML. Please read the Groovy section for detailed explanation of +what the types mean

    For YAML the structure of a matcher looks like this

    - path: $.foo
    +  type: by_regex
    +  value: bar
    +  regexType: as_string

    Or if you want to use one of the predefined regular expressions +[only_alpha_unicode, number, any_boolean, ip_address, hostname, +email, url, uuid, iso_date, iso_date_time, iso_time, iso_8601_with_offset, non_empty, non_blank]:

    - path: $.foo
    +  type: by_regex
    +  predefined: only_alpha_unicode

    Below you can find the allowed list of `type`s.

    • For stubMatchers:

      • by_equality
      • by_regex
      • by_date
      • by_timestamp
      • by_time
      • by_type

        • there are 2 additional fields accepted: minOccurrence and maxOccurrence.
    • For testMatchers:

      • by_equality
      • by_regex
      • by_date
      • by_timestamp
      • by_time
      • by_type

        • there are 2 additional fields accepted: minOccurrence and maxOccurrence.
      • by_command
      • by_null

    You can also define which type the regular expression corresponds to via the regexType field. Below you can find the allowed list of regular expression types:

    • as_integer
    • as_double
    • as_float,
    • as_long
    • as_short
    • as_boolean
    • as_string

    Consider the following example:

    Groovy DSL.  +

    Contract contractDsl = Contract.make {
    +	request {
    +		method 'GET'
    +		urlPath '/get'
    +		body([
    +				duck                : 123,
    +				alpha               : 'abc',
    +				number              : 123,
    +				aBoolean            : true,
    +				date                : '2017-01-01',
    +				dateTime            : '2017-01-01T01:23:45',
    +				time                : '01:02:34',
    +				valueWithoutAMatcher: 'foo',
    +				valueWithTypeMatch  : 'string',
    +				key                 : [
    +						'complex.key': 'foo'
    +				]
    +		])
    +		bodyMatchers {
    +			jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
    +			jsonPath('$.duck', byEquality())
    +			jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
    +			jsonPath('$.alpha', byEquality())
    +			jsonPath('$.number', byRegex(number()).asInteger())
    +			jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
    +			jsonPath('$.date', byDate())
    +			jsonPath('$.dateTime', byTimestamp())
    +			jsonPath('$.time', byTime())
    +			jsonPath("\$.['key'].['complex.key']", byEquality())
    +		}
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +	response {
    +		status OK()
    +		body([
    +				duck                 : 123,
    +				alpha                : 'abc',
    +				number               : 123,
    +				positiveInteger      : 1234567890,
    +				negativeInteger      : -1234567890,
    +				positiveDecimalNumber: 123.4567890,
    +				negativeDecimalNumber: -123.4567890,
    +				aBoolean             : true,
    +				date                 : '2017-01-01',
    +				dateTime             : '2017-01-01T01:23:45',
    +				time                 : "01:02:34",
    +				valueWithoutAMatcher : 'foo',
    +				valueWithTypeMatch   : 'string',
    +				valueWithMin         : [
    +						1, 2, 3
    +				],
    +				valueWithMax         : [
    +						1, 2, 3
    +				],
    +				valueWithMinMax      : [
    +						1, 2, 3
    +				],
    +				valueWithMinEmpty    : [],
    +				valueWithMaxEmpty    : [],
    +				key                  : [
    +						'complex.key': 'foo'
    +				],
    +				nullValue            : null
    +		])
    +		bodyMatchers {
    +			// asserts the jsonpath value against manual regex
    +			jsonPath('$.duck', byRegex("[0-9]{3}").asInteger())
    +			// asserts the jsonpath value against the provided value
    +			jsonPath('$.duck', byEquality())
    +			// asserts the jsonpath value against some default regex
    +			jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString())
    +			jsonPath('$.alpha', byEquality())
    +			jsonPath('$.number', byRegex(number()).asInteger())
    +			jsonPath('$.positiveInteger', byRegex(anInteger()).asInteger())
    +			jsonPath('$.negativeInteger', byRegex(anInteger()).asInteger())
    +			jsonPath('$.positiveDecimalNumber', byRegex(aDouble()).asDouble())
    +			jsonPath('$.negativeDecimalNumber', byRegex(aDouble()).asDouble())
    +			jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType())
    +			// asserts vs inbuilt time related regex
    +			jsonPath('$.date', byDate())
    +			jsonPath('$.dateTime', byTimestamp())
    +			jsonPath('$.time', byTime())
    +			// asserts that the resulting type is the same as in response body
    +			jsonPath('$.valueWithTypeMatch', byType())
    +			jsonPath('$.valueWithMin', byType {
    +				// results in verification of size of array (min 1)
    +				minOccurrence(1)
    +			})
    +			jsonPath('$.valueWithMax', byType {
    +				// results in verification of size of array (max 3)
    +				maxOccurrence(3)
    +			})
    +			jsonPath('$.valueWithMinMax', byType {
    +				// results in verification of size of array (min 1 & max 3)
    +				minOccurrence(1)
    +				maxOccurrence(3)
    +			})
    +			jsonPath('$.valueWithMinEmpty', byType {
    +				// results in verification of size of array (min 0)
    +				minOccurrence(0)
    +			})
    +			jsonPath('$.valueWithMaxEmpty', byType {
    +				// results in verification of size of array (max 0)
    +				maxOccurrence(0)
    +			})
    +			// will execute a method `assertThatValueIsANumber`
    +			jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
    +			jsonPath("\$.['key'].['complex.key']", byEquality())
    +			jsonPath('$.nullValue', byNull())
    +		}
    +		headers {
    +			contentType(applicationJson())
    +			header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
    +		}
    +	}
    +}

    +

    YAML.  +

    request:
    +  method: GET
    +  urlPath: /get/1
    +  headers:
    +    Content-Type: application/json
    +  cookies:
    +    foo: 2
    +    bar: 3
    +  queryParameters:
    +    limit: 10
    +    offset: 20
    +    filter: 'email'
    +    sort: name
    +    search: 55
    +    age: 99
    +    name: John.Doe
    +    email: 'bob@email.com'
    +  body:
    +    duck: 123
    +    alpha: "abc"
    +    number: 123
    +    aBoolean: true
    +    date: "2017-01-01"
    +    dateTime: "2017-01-01T01:23:45"
    +    time: "01:02:34"
    +    valueWithoutAMatcher: "foo"
    +    valueWithTypeMatch: "string"
    +    key:
    +      "complex.key": 'foo'
    +    nullValue: null
    +    valueWithMin:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinEmpty: []
    +    valueWithMaxEmpty: []
    +  matchers:
    +    url:
    +      regex: /get/[0-9]
    +      # predefined:
    +      # execute a method
    +      #command: 'equals($it)'
    +    queryParameters:
    +      - key: limit
    +        type: equal_to
    +        value: 20
    +      - key: offset
    +        type: containing
    +        value: 20
    +      - key: sort
    +        type: equal_to
    +        value: name
    +      - key: search
    +        type: not_matching
    +        value: '^[0-9]{2}$'
    +      - key: age
    +        type: not_matching
    +        value: '^\\w*$'
    +      - key: name
    +        type: matching
    +        value: 'John.*'
    +      - key: hello
    +        type: absent
    +    cookies:
    +      - key: foo
    +        regex: '[0-9]'
    +      - key: bar
    +        command: 'equals($it)'
    +    headers:
    +      - key: Content-Type
    +        regex: "application/json.*"
    +    body:
    +      - path: $.duck
    +        type: by_regex
    +        value: "[0-9]{3}"
    +      - path: $.duck
    +        type: by_equality
    +      - path: $.alpha
    +        type: by_regex
    +        predefined: only_alpha_unicode
    +      - path: $.alpha
    +        type: by_equality
    +      - path: $.number
    +        type: by_regex
    +        predefined: number
    +      - path: $.aBoolean
    +        type: by_regex
    +        predefined: any_boolean
    +      - path: $.date
    +        type: by_date
    +      - path: $.dateTime
    +        type: by_timestamp
    +      - path: $.time
    +        type: by_time
    +      - path: "$.['key'].['complex.key']"
    +        type: by_equality
    +      - path: $.nullvalue
    +        type: by_null
    +      - path: $.valueWithMin
    +        type: by_type
    +        minOccurrence: 1
    +      - path: $.valueWithMax
    +        type: by_type
    +        maxOccurrence: 3
    +      - path: $.valueWithMinMax
    +        type: by_type
    +        minOccurrence: 1
    +        maxOccurrence: 3
    +response:
    +  status: 200
    +  cookies:
    +    foo: 1
    +    bar: 2
    +  body:
    +    duck: 123
    +    alpha: "abc"
    +    number: 123
    +    aBoolean: true
    +    date: "2017-01-01"
    +    dateTime: "2017-01-01T01:23:45"
    +    time: "01:02:34"
    +    valueWithoutAMatcher: "foo"
    +    valueWithTypeMatch: "string"
    +    valueWithMin:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinMax:
    +      - 1
    +      - 2
    +      - 3
    +    valueWithMinEmpty: []
    +    valueWithMaxEmpty: []
    +    key:
    +      'complex.key': 'foo'
    +    nulValue: null
    +  matchers:
    +    headers:
    +      - key: Content-Type
    +        regex: "application/json.*"
    +    cookies:
    +      - key: foo
    +        regex: '[0-9]'
    +      - key: bar
    +        command: 'equals($it)'
    +    body:
    +      - path: $.duck
    +        type: by_regex
    +        value: "[0-9]{3}"
    +      - path: $.duck
    +        type: by_equality
    +      - path: $.alpha
    +        type: by_regex
    +        predefined: only_alpha_unicode
    +      - path: $.alpha
    +        type: by_equality
    +      - path: $.number
    +        type: by_regex
    +        predefined: number
    +      - path: $.aBoolean
    +        type: by_regex
    +        predefined: any_boolean
    +      - path: $.date
    +        type: by_date
    +      - path: $.dateTime
    +        type: by_timestamp
    +      - path: $.time
    +        type: by_time
    +      - path: $.valueWithTypeMatch
    +        type: by_type
    +      - path: $.valueWithMin
    +        type: by_type
    +        minOccurrence: 1
    +      - path: $.valueWithMax
    +        type: by_type
    +        maxOccurrence: 3
    +      - path: $.valueWithMinMax
    +        type: by_type
    +        minOccurrence: 1
    +        maxOccurrence: 3
    +      - path: $.valueWithMinEmpty
    +        type: by_type
    +        minOccurrence: 0
    +      - path: $.valueWithMaxEmpty
    +        type: by_type
    +        maxOccurrence: 0
    +      - path: $.duck
    +        type: by_command
    +        value: assertThatValueIsANumber($it)
    +      - path: $.nullValue
    +        type: by_null
    +        value: null
    +  headers:
    +    Content-Type: application/json

    +

    In the preceding example, you can see the dynamic portions of the contract in the +matchers sections. For the request part, you can see that, for all fields but +valueWithoutAMatcher, the values of the regular expressions that the stub should +contain are explicitly set. For the valueWithoutAMatcher, the verification takes place +in the same way as without the use of matchers. In that case, the test performs an +equality check.

    For the response side in the bodyMatchers section, we define the dynamic parts in a +similar manner. The only difference is that the byType matchers are also present. The +verifier engine checks four fields to verify whether the response from the test +has a value for which the JSON path matches the given field, is of the same type as the one +defined in the response body, and passes the following check (based on the method being called):

    • For $.valueWithTypeMatch, the engine checks whether the type is the same.
    • For $.valueWithMin, the engine check the type and asserts whether the size is greater +than or equal to the minimum occurrence.
    • For $.valueWithMax, the engine checks the type and asserts whether the size is +smaller than or equal to the maximum occurrence.
    • For $.valueWithMinMax, the engine checks the type and asserts whether the size is +between the min and maximum occurrence.

    The resulting test would resemble the following example (note that an and section +separates the autogenerated assertions and the assertion from matchers):

    // given:
    + MockMvcRequestSpecification request = given()
    +   .header("Content-Type", "application/json")
    +   .body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\",\"key\":{\"complex.key\":\"foo\"}}");
    +
    +// when:
    + ResponseOptions response = given().spec(request)
    +   .get("/get");
    +
    +// then:
    + assertThat(response.statusCode()).isEqualTo(200);
    + assertThat(response.header("Content-Type")).matches("application/json.*");
    +// and:
    + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    + assertThatJson(parsedJson).field("['valueWithoutAMatcher']").isEqualTo("foo");
    +// and:
    + assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}");
    + assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123);
    + assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*");
    + assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc");
    + assertThat(parsedJson.read("$.number", String.class)).matches("-?(\\d*\\.\\d+|\\d+)");
    + assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)");
    + assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])");
    + assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
    + assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])");
    + assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class);
    + assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).as("$.valueWithMin").hasSizeGreaterThanOrEqualTo(1);
    + assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).as("$.valueWithMax").hasSizeLessThanOrEqualTo(3);
    + assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).as("$.valueWithMinMax").hasSizeBetween(1, 3);
    + assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).as("$.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0);
    + assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class);
    + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0);
    + assertThatValueIsANumber(parsedJson.read("$.duck"));
    + assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo");
    [Important]Important

    Notice that, for the byCommand method, the example calls the +assertThatValueIsANumber. This method must be defined in the test base class or be +statically imported to your tests. Notice that the byCommand call was converted to +assertThatValueIsANumber(parsedJson.read("$.duck"));. That means that the engine took +the method name and passed the proper JSON path as a parameter to it.

    The resulting WireMock stub is in the following example:

    					'''
    +{
    +  "request" : {
    +    "urlPath" : "/get",
    +    "method" : "POST",
    +    "headers" : {
    +      "Content-Type" : {
    +        "matches" : "application/json.*"
    +      }
    +    },
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]"
    +    }, {
    +      "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]"
    +    }, {
    +      "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.duck == 123)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.alpha == 'abc')]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\d+|\\\\d+))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.dateTime =~ /(([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]"
    +    }, {
    +      "matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithMin.size() >= 1)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithMax.size() <= 3)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithMinMax.size() >= 1 && @.valueWithMinMax.size() <= 3)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.valueWithOccurrence.size() >= 4 && @.valueWithOccurrence.size() <= 4)]"
    +    } ]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "{\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"number\\":123,\\"aBoolean\\":true,\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"time\\":\\"01:02:34\\",\\"valueWithoutAMatcher\\":\\"foo\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMin\\":[1,2,3],\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithOccurrence\\":[1,2,3,4]}",
    +    "headers" : {
    +      "Content-Type" : "application/json"
    +    },
    +    "transformers" : [ "response-template" ]
    +  }
    +}
    +'''
    [Important]Important

    If you use a matcher, then the part of the request and response that the +matcher addresses with the JSON Path gets removed from the assertion. In the case of +verifying a collection, you must create matchers for all the elements of the +collection.

    Consider the following example:

    Contract.make {
    +    request {
    +        method 'GET'
    +        url("/foo")
    +    }
    +    response {
    +        status OK()
    +        body(events: [[
    +                                 operation          : 'EXPORT',
    +                                 eventId            : '16f1ed75-0bcc-4f0d-a04d-3121798faf99',
    +                                 status             : 'OK'
    +                         ], [
    +                                 operation          : 'INPUT_PROCESSING',
    +                                 eventId            : '3bb4ac82-6652-462f-b6d1-75e424a0024a',
    +                                 status             : 'OK'
    +                         ]
    +                ]
    +        )
    +        bodyMatchers {
    +            jsonPath('$.events[0].operation', byRegex('.+'))
    +            jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$'))
    +            jsonPath('$.events[0].status', byRegex('.+'))
    +        }
    +    }
    +}

    The preceding code leads to creating the following test (the code block shows only the assertion section):

    and:
    +	DocumentContext parsedJson = JsonPath.parse(response.body.asString())
    +	assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("16f1ed75-0bcc-4f0d-a04d-3121798faf99")
    +	assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("EXPORT")
    +	assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("INPUT_PROCESSING")
    +	assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("3bb4ac82-6652-462f-b6d1-75e424a0024a")
    +	assertThatJson(parsedJson).array("['events']").contains("['status']").isEqualTo("OK")
    +and:
    +	assertThat(parsedJson.read("\$.events[0].operation", String.class)).matches(".+")
    +	assertThat(parsedJson.read("\$.events[0].eventId", String.class)).matches("^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\$")
    +	assertThat(parsedJson.read("\$.events[0].status", String.class)).matches(".+")

    As you can see, the assertion is malformed. Only the first element of the array got +asserted. In order to fix this, you should apply the assertion to the whole $.events +collection and assert it with the byCommand(…​) method.

    94.6 JAX-RS Support

    The Spring Cloud Contract Verifier supports the JAX-RS 2 Client API. The base class needs +to define protected WebTarget webTarget and server initialization. The only option for +testing JAX-RS API is to start a web server. Also, a request with a body needs to have a +content type set. Otherwise, the default of application/octet-stream gets used.

    In order to use JAX-RS mode, use the following settings:

    testMode == 'JAXRSCLIENT'

    The following example shows a generated test API:

    					'''
    + // when:
    +  Response response = webTarget
    +    .path("/users")
    +    .queryParam("limit", "10")
    +    .queryParam("offset", "20")
    +    .queryParam("filter", "email")
    +    .queryParam("sort", "name")
    +    .queryParam("search", "55")
    +    .queryParam("age", "99")
    +    .queryParam("name", "Denis.Stepanov")
    +    .queryParam("email", "bob@email.com")
    +    .request()
    +    .method("GET");
    +
    +  String responseAsString = response.readEntity(String.class);
    +
    + // then:
    +  assertThat(response.getStatus()).isEqualTo(200);
    + // and:
    +  DocumentContext parsedJson = JsonPath.parse(responseAsString);
    +  assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
    +'''

    94.7 Async Support

    If you’re using asynchronous communication on the server side (your controllers are +returning Callable, DeferredResult, and so on), then, inside your contract, you must +provide an async() method in the response section. The following code shows an example:

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +    request {
    +        method GET()
    +        url '/get'
    +    }
    +    response {
    +        status OK()
    +        body 'Passed'
    +        async()
    +    }
    +}

    +

    YAML.  +

    response:
    +    async: true

    +

    You can also use the fixedDelayMilliseconds method / property to add delay to your stubs.

    Groovy DSL.  +

    org.springframework.cloud.contract.spec.Contract.make {
    +    request {
    +        method GET()
    +        url '/get'
    +    }
    +    response {
    +        status 200
    +        body 'Passed'
    +        fixedDelayMilliseconds 1000
    +    }
    +}

    +

    YAML.  +

    response:
    +    fixedDelayMilliseconds: 1000

    +

    94.8 Working with Context Paths

    Spring Cloud Contract supports context paths.

    [Important]Important

    The only change needed to fully support context paths is the switch on the +PRODUCER side. Also, the autogenerated tests must use EXPLICIT mode. The consumer +side remains untouched. In order for the generated test to pass, you must use EXPLICIT +mode.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <testMode>EXPLICIT</testMode>
    +    </configuration>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		testMode = 'EXPLICIT'
    +}

    +

    That way, you generate a test that DOES NOT use MockMvc. It means that you generate +real requests and you need to setup your generated test’s base class to work on a real +socket.

    Consider the following contract:

    org.springframework.cloud.contract.spec.Contract.make {
    +	request {
    +		method 'GET'
    +		url '/my-context-path/url'
    +	}
    +	response {
    +		status OK()
    +	}
    +}

    The following example shows how to set up a base class and Rest Assured:

    import io.restassured.RestAssured;
    +import org.junit.Before;
    +import org.springframework.boot.web.server.LocalServerPort;
    +import org.springframework.boot.test.context.SpringBootTest;
    +
    +@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    +class ContextPathTestingBaseClass {
    +
    +	@LocalServerPort int port;
    +
    +	@Before
    +	public void setup() {
    +		RestAssured.baseURI = "http://localhost";
    +		RestAssured.port = this.port;
    +	}
    +}

    If you do it this way:

    • All of your requests in the autogenerated tests are sent to the real endpoint with your +context path included (for example, /my-context-path/url).
    • Your contracts reflect that you have a context path. Your generated stubs also have +that information (for example, in the stubs, you have to call /my-context-path/url).

    94.9 Working with WebFlux

    Spring Cloud Contract offers two ways of working with WebFlux.

    94.9.1 WebFlux with WebTestClient

    One of them is via the WebTestClient mode.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <testMode>WEBTESTCLIENT</testMode>
    +    </configuration>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		testMode = 'WEBTESTCLIENT'
    +}

    +

    The following example shows how to set up a WebTestClient base class and RestAssured +for WebFlux:

    import io.restassured.module.webtestclient.RestAssuredWebTestClient;
    +import org.junit.Before;
    +
    +public abstract class BeerRestBase {
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredWebTestClient.standaloneSetup(
    +		new ProducerController(personToCheck -> personToCheck.age >= 20));
    +	}
    +}
    +}

    94.9.2 WebFlux with Explicit mode

    Another way is with the EXPLICIT mode in your generated tests +to work with WebFlux.

    Maven.  +

    <plugin>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +    <version>${spring-cloud-contract.version}</version>
    +    <extensions>true</extensions>
    +    <configuration>
    +        <testMode>EXPLICIT</testMode>
    +    </configuration>
    +</plugin>

    +

    Gradle.  +

    contracts {
    +		testMode = 'EXPLICIT'
    +}

    +

    The following example shows how to set up a base class and Rest Assured for Web Flux:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(classes = BeerRestBase.Config.class,
    +		webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    +		properties = "server.port=0")
    +public abstract class BeerRestBase {
    +
    +    // your tests go here
    +
    +    // in this config class you define all controllers and mocked services
    +@Configuration
    +@EnableAutoConfiguration
    +static class Config {
    +
    +	@Bean
    +	PersonCheckingService personCheckingService()  {
    +		return personToCheck -> personToCheck.age >= 20;
    +	}
    +
    +	@Bean
    +	ProducerController producerController() {
    +		return new ProducerController(personCheckingService());
    +	}
    +}
    +
    +}

    94.10 XML Support for REST

    For REST contracts, we also support XML request and response body. +The XML body has to be passed within the body element +as a String or GString. Also body matchers can be provided for +both request and response. In place of the jsonPath(…​) method, the org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath +method should be used, with the desired xPath provided as the first argument +and the appropriate MatchingType as second. All the body matchers apart from byType() are supported.

    Here is an example of a Groovy DSL contract with XML response body:

    					Contract.make {
    +						request {
    +							method GET()
    +							urlPath '/get'
    +							headers {
    +								contentType(applicationXml())
    +							}
    +						}
    +						response {
    +							status(OK())
    +							headers {
    +								contentType(applicationXml())
    +							}
    +							body """
    +<test>
    +<duck type='xtype'>123</duck>
    +<alpha>abc</alpha>
    +<list>
    +<elem>abc</elem>
    +<elem>def</elem>
    +<elem>ghi</elem>
    +</list>
    +<number>123</number>
    +<aBoolean>true</aBoolean>
    +<date>2017-01-01</date>
    +<dateTime>2017-01-01T01:23:45</dateTime>
    +<time>01:02:34</time>
    +<valueWithoutAMatcher>foo</valueWithoutAMatcher>
    +<key><complex>foo</complex></key>
    +</test>"""
    +							bodyMatchers {
    +								xPath('/test/duck/text()', byRegex("[0-9]{3}"))
    +								xPath('/test/duck/text()', byCommand('test($it)'))
    +								xPath('/test/duck/xxx', byNull())
    +								xPath('/test/duck/text()', byEquality())
    +								xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode()))
    +								xPath('/test/alpha/text()', byEquality())
    +								xPath('/test/number/text()', byRegex(number()))
    +								xPath('/test/date/text()', byDate())
    +								xPath('/test/dateTime/text()', byTimestamp())
    +								xPath('/test/time/text()', byTime())
    +								xPath('/test/*/complex/text()', byEquality())
    +								xPath('/test/duck/@type', byEquality())
    +							}
    +						}
    +					}

    And below is an example of a YAML contract with XML request and response bodies:

    include::{verifier_core_path}/src/test/resources/yml/contract_rest_xml.yml

    Here is an example of an automatically generated test for XML response body:

    @Test
    +public void validate_xmlMatches() throws Exception {
    +	// given:
    +	MockMvcRequestSpecification request = given()
    +				.header("Content-Type", "application/xml");
    +
    +	// when:
    +	ResponseOptions response = given().spec(request).get("/get");
    +
    +	// then:
    +	assertThat(response.statusCode()).isEqualTo(200);
    +	// and:
    +	DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance()
    +					.newDocumentBuilder();
    +	Document parsedXml = documentBuilder.parse(new InputSource(
    +				new StringReader(response.getBody().asString())));
    +	// and:
    +	assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc");
    +	assertThat(valueFromXPath(parsedXml,"/test/list/elem[2]/text()")).isEqualTo("def");
    +	assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]{3}");
    +	assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull();
    +	assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\p{L}]*");
    +	assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo");
    +	assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype");
    +	}

    94.11 Messaging Top-Level Elements

    The DSL for messaging looks a little bit different than the one that focuses on HTTP. The +following sections explain the differences:

    94.11.1 Output Triggered by a Method

    The output message can be triggered by calling a method (such as a Scheduler when a was +started and a message was sent), as shown in the following example:

    Groovy DSL.  +

    def dsl = Contract.make {
    +	// Human readable description
    +	description 'Some description'
    +	// Label by means of which the output message can be triggered
    +	label 'some_label'
    +	// input to the contract
    +	input {
    +		// the contract will be triggered by a method
    +		triggeredBy('bookReturnedTriggered()')
    +	}
    +	// output message of the contract
    +	outputMessage {
    +		// destination to which the output message will be sent
    +		sentTo('output')
    +		// the body of the output message
    +		body('''{ "bookName" : "foo" }''')
    +		// the headers of the output message
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    +

    YAML.  +

    # Human readable description
    +description: Some description
    +# Label by means of which the output message can be triggered
    +label: some_label
    +input:
    +  # the contract will be triggered by a method
    +  triggeredBy: bookReturnedTriggered()
    +# output message of the contract
    +outputMessage:
    +  # destination to which the output message will be sent
    +  sentTo: output
    +  # the body of the output message
    +  body:
    +    bookName: foo
    +  # the headers of the output message
    +  headers:
    +    BOOK-NAME: foo

    +

    In the previous example case, the output message is sent to output if a method called +bookReturnedTriggered is executed. On the message publisher’s side, we generate a +test that calls that method to trigger the message. On the consumer side, you can use +the some_label to trigger the message.

    94.11.2 Output Triggered by a Message

    The output message can be triggered by receiving a message, as shown in the following +example:

    Groovy DSL.  +

    def dsl = Contract.make {
    +	description 'Some Description'
    +	label 'some_label'
    +	// input is a message
    +	input {
    +		// the message was received from this destination
    +		messageFrom('input')
    +		// has the following body
    +		messageBody([
    +				bookName: 'foo'
    +		])
    +		// and the following headers
    +		messageHeaders {
    +			header('sample', 'header')
    +		}
    +	}
    +	outputMessage {
    +		sentTo('output')
    +		body([
    +				bookName: 'foo'
    +		])
    +		headers {
    +			header('BOOK-NAME', 'foo')
    +		}
    +	}
    +}

    +

    YAML.  +

    # Human readable description
    +description: Some description
    +# Label by means of which the output message can be triggered
    +label: some_label
    +# input is a message
    +input:
    +  messageFrom: input
    +  # has the following body
    +  messageBody:
    +    bookName: 'foo'
    +  # and the following headers
    +  messageHeaders:
    +    sample: 'header'
    +# output message of the contract
    +outputMessage:
    +  # destination to which the output message will be sent
    +  sentTo: output
    +  # the body of the output message
    +  body:
    +    bookName: foo
    +  # the headers of the output message
    +  headers:
    +    BOOK-NAME: foo

    +

    In the preceding example, the output message is sent to output if a proper message is +received on the input destination. On the message publisher’s side, the engine +generates a test that sends the input message to the defined destination. On the +consumer side, you can either send a message to the input destination or use a label +(some_label in the example) to trigger the message.

    94.11.3 Consumer/Producer

    [Important]Important

    This section is valid only for Groovy DSL.

    In HTTP, you have a notion of client/stub and `server/test notation. You can also +use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also +provides the consumer and producer methods, as presented in the following example +(note that you can use either $ or value methods to provide consumer and producer +parts):

    					Contract.make {
    +						label 'some_label'
    +						input {
    +							messageFrom value(consumer('jms:output'), producer('jms:input'))
    +							messageBody([
    +									bookName: 'foo'
    +							])
    +							messageHeaders {
    +								header('sample', 'header')
    +							}
    +						}
    +						outputMessage {
    +							sentTo $(consumer('jms:input'), producer('jms:output'))
    +							body([
    +									bookName: 'foo'
    +							])
    +						}
    +					}

    94.11.4 Common

    In the input or outputMessage section you can call assertThat with the name +of a method (e.g. assertThatMessageIsOnTheQueue()) that you have defined in the +base class or in a static import. Spring Cloud Contract will execute that method +in the generated test.

    94.12 Multiple Contracts in One File

    You can define multiple contracts in one file. Such a contract might resemble the +following example:

    Groovy DSL.  +

    import org.springframework.cloud.contract.spec.Contract
    +
    +[
    +	Contract.make {
    +		name("should post a user")
    +		request {
    +			method 'POST'
    +			url('/users/1')
    +		}
    +		response {
    +			status OK()
    +		}
    +	},
    +	Contract.make {
    +		request {
    +			method 'POST'
    +			url('/users/2')
    +		}
    +		response {
    +			status OK()
    +		}
    +	}
    +]

    +

    YAML.  +

    ---
    +name: should post a user
    +request:
    +  method: POST
    +  url: /users/1
    +response:
    +  status: 200
    +---
    +request:
    +  method: POST
    +  url: /users/2
    +response:
    +  status: 200
    +---
    +request:
    +  method: POST
    +  url: /users/3
    +response:
    +  status: 200

    +

    In the preceding example, one contract has the name field and the other does not. This +leads to generation of two tests that look more or less like this:

    package org.springframework.cloud.contract.verifier.tests.com.hello;
    +
    +import com.example.TestBase;
    +import com.jayway.jsonpath.DocumentContext;
    +import com.jayway.jsonpath.JsonPath;
    +import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
    +import com.jayway.restassured.response.ResponseOptions;
    +import org.junit.Test;
    +
    +import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;
    +import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +public class V1Test extends TestBase {
    +
    +	@Test
    +	public void validate_should_post_a_user() throws Exception {
    +		// given:
    +			MockMvcRequestSpecification request = given();
    +
    +		// when:
    +			ResponseOptions response = given().spec(request)
    +					.post("/users/1");
    +
    +		// then:
    +			assertThat(response.statusCode()).isEqualTo(200);
    +	}
    +
    +	@Test
    +	public void validate_withList_1() throws Exception {
    +		// given:
    +			MockMvcRequestSpecification request = given();
    +
    +		// when:
    +			ResponseOptions response = given().spec(request)
    +					.post("/users/2");
    +
    +		// then:
    +			assertThat(response.statusCode()).isEqualTo(200);
    +	}
    +
    +}

    Notice that, for the contract that has the name field, the generated test method is named +validate_should_post_a_user. For the one that does not have the name, it is called +validate_withList_1. It corresponds to the name of the file WithList.groovy and the +index of the contract in the list.

    The generated stubs is shown in the following example:

    should post a user.json
    +1_WithList.json

    As you can see, the first file got the name parameter from the contract. The second +got the name of the contract file (WithList.groovy) prefixed with the index (in this +case, the contract had an index of 1 in the list of contracts in the file).

    [Tip]Tip

    As you can see, it is much better if you name your contracts because doing so makes +your tests far more meaningful.

    94.13 Generating Spring REST Docs snippets from the contracts

    When you want to include the requests and responses of your API using Spring REST Docs, +you only need to make some minor changes to your setup if you are using MockMvc and RestAssuredMockMvc. +Simply include the following dependencies if you haven’t already.

    Maven.  +

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
    +	<scope>test</scope>
    +</dependency>
    +<dependency>
    +	<groupId>org.springframework.restdocs</groupId>
    +	<artifactId>spring-restdocs-mockmvc</artifactId>
    +	<optional>true</optional>
    +</dependency>

    +

    Gradle.  +

    testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
    +testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc'

    +

    Next you need to make some changes to your base class like the following example.

    package com.example.fraud;
    +
    +import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.rules.TestName;
    +import org.junit.runner.RunWith;
    +
    +import org.springframework.beans.factory.annotation.Autowired;
    +import org.springframework.boot.test.context.SpringBootTest;
    +import org.springframework.restdocs.JUnitRestDocumentation;
    +import org.springframework.test.context.junit4.SpringRunner;
    +import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    +import org.springframework.web.context.WebApplicationContext;
    +
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
    +
    +@RunWith(SpringRunner.class)
    +@SpringBootTest(classes = Application.class)
    +public abstract class FraudBaseWithWebAppSetup {
    +
    +	private static final String OUTPUT = "target/generated-snippets";
    +
    +	@Rule
    +	public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
    +
    +	@Rule
    +	public TestName testName = new TestName();
    +
    +	@Autowired
    +	private WebApplicationContext context;
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(this.context)
    +				.apply(documentationConfiguration(this.restDocumentation))
    +				.alwaysDo(document(
    +						getClass().getSimpleName() + "_" + testName.getMethodName()))
    +				.build());
    +	}
    +
    +	protected void assertThatRejectionReasonIsNull(Object rejectionReason) {
    +		assert rejectionReason == null;
    +	}
    +
    +}

    In case you are using the standalone setup, you can set up RestAssuredMockMvc like this:

    package com.example.fraud;
    +
    +import io.restassured.module.mockmvc.RestAssuredMockMvc;
    +import org.junit.Before;
    +import org.junit.Rule;
    +import org.junit.rules.TestName;
    +
    +import org.springframework.restdocs.JUnitRestDocumentation;
    +import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    +
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
    +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
    +
    +public abstract class FraudBaseWithStandaloneSetup {
    +
    +	private static final String OUTPUT = "target/generated-snippets";
    +
    +	@Rule
    +	public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT);
    +
    +	@Rule
    +	public TestName testName = new TestName();
    +
    +	@Before
    +	public void setup() {
    +		RestAssuredMockMvc.standaloneSetup(MockMvcBuilders
    +				.standaloneSetup(new FraudDetectionController())
    +				.apply(documentationConfiguration(this.restDocumentation))
    +				.alwaysDo(document(
    +						getClass().getSimpleName() + "_" + testName.getMethodName())));
    +	}
    +
    +}
    [Tip]Tip

    You don’t need to specify the output directory for the generated snippets since version 1.2.0.RELEASE of Spring REST Docs.

    95. Customization

    [Important]Important

    This section is valid only for Groovy DSL

    You can customize the Spring Cloud Contract Verifier by extending the DSL, as shown in +the remainder of this section.

    95.1 Extending the DSL

    You can provide your own functions to the DSL. The key requirement for this feature is to +maintain the static compatibility. Later in this document, you can see examples of:

    • Creating a JAR with reusable classes.
    • Referencing of these classes in the DSLs.

    You can find the full example +here.

    95.1.1 Common JAR

    The following examples show three classes that can be reused in the DSLs.

    PatternUtils contains functions used by both the consumer and the producer.

    package com.example;
    +
    +import java.util.regex.Pattern;
    +
    +/**
    + * If you want to use {@link Pattern} directly in your tests
    + * then you can create a class resembling this one. It can
    + * contain all the {@link Pattern} you want to use in the DSL.
    + *
    + * <pre>
    + * {@code
    + * request {
    + *     body(
    + *         [ age: $(c(PatternUtils.oldEnough()))]
    + *     )
    + * }
    + * </pre>
    + *
    + * Notice that we're using both {@code $()} for dynamic values
    + * and {@code c()} for the consumer side.
    + *
    + * @author Marcin Grzejszczak
    + */
    +//tag::impl[]
    +public class PatternUtils {
    +
    +	public static String tooYoung() {
    +		//remove::start[]
    +		return "[0-1][0-9]";
    +		//remove::end[return]
    +	}
    +
    +	public static Pattern oldEnough() {
    +		//remove::start[]
    +		return Pattern.compile("[2-9][0-9]");
    +		//remove::end[return]
    +	}
    +
    +	/**
    +	 * Makes little sense but it's just an example ;)
    +	 */
    +	public static Pattern ok() {
    +		//remove::start[]
    +		return Pattern.compile("OK");
    +		//remove::end[return]
    +	}
    +}
    +//end::impl[]

    ConsumerUtils contains functions used by the consumer.

    package com.example;
    +
    +import org.springframework.cloud.contract.spec.internal.ClientDslProperty;
    +
    +/**
    + * DSL Properties passed to the DSL from the consumer's perspective.
    + * That means that on the input side {@code Request} for HTTP
    + * or {@code Input} for messaging you can have a regular expression.
    + * On the {@code Response} for HTTP or {@code Output} for messaging
    + * you have to have a concrete value.
    + *
    + * @author Marcin Grzejszczak
    + */
    +//tag::impl[]
    +public class ConsumerUtils {
    +	/**
    +	 * Consumer side property. By using the {@link ClientDslProperty}
    +	 * you can omit most of boilerplate code from the perspective
    +	 * of dynamic values. Example
    +	 *
    +	 * <pre>
    +	 * {@code
    +	 * request {
    +	 *     body(
    +	 *         [ age: $(ConsumerUtils.oldEnough())]
    +	 *     )
    +	 * }
    +	 * </pre>
    +	 *
    +	 * That way it's in the implementation that we decide what value we will pass to the consumer
    +	 * and which one to the producer.
    +	 *
    +	 * @author Marcin Grzejszczak
    +	 */
    +	public static ClientDslProperty oldEnough() {
    +		//remove::start[]
    +		// this example is not the best one and
    +		// theoretically you could just pass the regex instead of `ServerDslProperty` but
    +		// it's just to show some new tricks :)
    +		return new ClientDslProperty(PatternUtils.oldEnough(), 40);
    +		//remove::end[return]
    +	}
    +
    +}
    +//end::impl[]

    ProducerUtils contains functions used by the producer.

    package com.example;
    +
    +import org.springframework.cloud.contract.spec.internal.ServerDslProperty;
    +
    +/**
    + * DSL Properties passed to the DSL from the producer's perspective.
    + * That means that on the input side {@code Request} for HTTP
    + * or {@code Input} for messaging you have to have a concrete value.
    + * On the {@code Response} for HTTP or {@code Output} for messaging
    + * you can have a regular expression.
    + *
    + * @author Marcin Grzejszczak
    + */
    +//tag::impl[]
    +public class ProducerUtils {
    +
    +	/**
    +	 * Producer side property. By using the {@link ProducerUtils}
    +	 * you can omit most of boilerplate code from the perspective
    +	 * of dynamic values. Example
    +	 *
    +	 * <pre>
    +	 * {@code
    +	 * response {
    +	 *     body(
    +	 *         [ status: $(ProducerUtils.ok())]
    +	 *     )
    +	 * }
    +	 * </pre>
    +	 *
    +	 * That way it's in the implementation that we decide what value we will pass to the consumer
    +	 * and which one to the producer.
    +	 */
    +	public static ServerDslProperty ok() {
    +		// this example is not the best one and
    +		// theoretically you could just pass the regex instead of `ServerDslProperty` but
    +		// it's just to show some new tricks :)
    +		return new ServerDslProperty( PatternUtils.ok(), "OK");
    +	}
    +}
    +//end::impl[]

    95.1.2 Adding the Dependency to the Project

    In order for the plugins and IDE to be able to reference the common JAR classes, you need +to pass the dependency to your project.

    95.1.3 Test the Dependency in the Project’s Dependencies

    First, add the common jar dependency as a test dependency. Because your contracts files +are available on the test resources path, the common jar classes automatically become +visible in your Groovy files. The following examples show how to test the dependency:

    Maven.  +

    <dependency>
    +	<groupId>com.example</groupId>
    +	<artifactId>beer-common</artifactId>
    +	<version>${project.version}</version>
    +	<scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile("com.example:beer-common:0.0.1.BUILD-SNAPSHOT")

    +

    95.1.4 Test a Dependency in the Plugin’s Dependencies

    Now, you must add the dependency for the plugin to reuse at runtime, as shown in the +following example:

    Maven.  +

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example</packageWithBaseClasses>
    +		<baseClassMappings>
    +			<baseClassMapping>
    +				<contractPackageRegex>.*intoxication.*</contractPackageRegex>
    +				<baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN>
    +			</baseClassMapping>
    +		</baseClassMappings>
    +	</configuration>
    +	<dependencies>
    +		<dependency>
    +			<groupId>com.example</groupId>
    +			<artifactId>beer-common</artifactId>
    +			<version>${project.version}</version>
    +			<scope>compile</scope>
    +		</dependency>
    +	</dependencies>
    +</plugin>

    +

    Gradle.  +

    classpath "com.example:beer-common:0.0.1.BUILD-SNAPSHOT"

    +

    95.1.5 Referencing classes in DSLs

    You can now reference your classes in your DSL, as shown in the following example:

    package contracts.beer.rest
    +
    +import com.example.ConsumerUtils
    +import com.example.ProducerUtils
    +import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +	description("""
    +Represents a successful scenario of getting a beer
    +
    +```
    +given:
    +	client is old enough
    +when:
    +	he applies for a beer
    +then:
    +	we'll grant him the beer
    +```
    +
    +""")
    +	request {
    +		method 'POST'
    +		url '/check'
    +		body(
    +				age: $(ConsumerUtils.oldEnough())
    +		)
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +	response {
    +		status 200
    +		body("""
    +			{
    +				"status": "${value(ProducerUtils.ok())}"
    +			}
    +			""")
    +		headers {
    +			contentType(applicationJson())
    +		}
    +	}
    +}
    [Important]Important

    You can set the Spring Cloud Contract plugin up by setting convertToYaml to true. That way you will NOT have to add the dependency with the extended functionality to the consumer side, since the consumer side will be using YAML contracts instead of Groovy ones.

    96. Using the Pluggable Architecture

    You may encounter cases where you have your contracts have been defined in other formats, +such as YAML, RAML or PACT. In those cases, you still want to benefit from the automatic +generation of tests and stubs. You can add your own implementation for generating both +tests and stubs. Also, you can customize the way tests are generated (for example, you +can generate tests for other languages) and the way stubs are generated (for example, you +can generate stubs for other HTTP server implementations).

    96.1 Custom Contract Converter

    The ContractConverter interface lets you register your own implementation of a contract +structure converter. The following code listing shows the ContractConverter interface:

    package org.springframework.cloud.contract.spec
    +
    +/**
    + * Converter to be used to convert FROM {@link File} TO {@link Contract}
    + * and from {@link Contract} to {@code T}
    + *
    + * @param <T >     - type to which we want to convert the contract
    + *
    + * @author Marcin Grzejszczak
    + * @since 1.1.0
    + */
    +interface ContractConverter<T> extends ContractStorer<T> {
    +
    +	/**
    +	 * Should this file be accepted by the converter. Can use the file extension
    +	 * to check if the conversion is possible.
    +	 *
    +	 * @param file - file to be considered for conversion
    +	 * @return - {@code true} if the given implementation can convert the file
    +	 */
    +	boolean isAccepted(File file)
    +
    +	/**
    +	 * Converts the given {@link File} to its {@link Contract} representation
    +	 *
    +	 * @param file - file to convert
    +	 * @return - {@link Contract} representation of the file
    +	 */
    +	Collection<Contract> convertFrom(File file)
    +
    +	/**
    +	 * Converts the given {@link Contract} to a {@link T} representation
    +	 *
    +	 * @param contract - the parsed contract
    +	 * @return - {@link T} the type to which we do the conversion
    +	 */
    +	T convertTo(Collection<Contract> contract)
    +}

    Your implementation must define the condition on which it should start the +conversion. Also, you must define how to perform that conversion in both directions.

    [Important]Important

    Once you create your implementation, you must create a +/META-INF/spring.factories file in which you provide the fully qualified name of your +implementation.

    The following example shows a typical spring.factories file:

    org.springframework.cloud.contract.spec.ContractConverter=\
    +org.springframework.cloud.contract.verifier.converter.YamlContractConverter

    96.1.1 Pact Converter

    Spring Cloud Contract includes support for Pact representation of +contracts up until v4. Instead of using the Groovy DSL, you can use Pact files. In this section, we +present how to add Pact support for your project. Note however that not all functionality is supported. +Starting with v3 you can combine multiple matcher for the same element; +you can use matchers for the body, headers, request and path; and you can use value generators. +Spring Cloud Contract currently only supports multiple matchers that are combined using the AND rule logic. +Next to that the request and path matchers are skipped during the conversion. +When using a date, time or datetime value generator with a given format, +the given format will be skipped and the ISO format will be used.

    In order to properly support the Spring Cloud Contract way of doing messaging +with Pact you’ll have to provide some additional meta data entries. Below you can find a list of such entries:

    • to define the destination to which a message gets sent, you have to +set a metaData entry in the Pact file, with key sentTo equal to the destination to which a message is to be sent. E.g. "metaData": { "sentTo": "activemq:output" }

    96.1.2 Pact Contract

    Consider following example of a Pact contract, which is a file under the +src/test/resources/contracts folder.

    {
    +  "provider": {
    +    "name": "Provider"
    +  },
    +  "consumer": {
    +    "name": "Consumer"
    +  },
    +  "interactions": [
    +    {
    +      "description": "",
    +      "request": {
    +        "method": "PUT",
    +        "path": "/fraudcheck",
    +        "headers": {
    +          "Content-Type": "application/vnd.fraud.v1+json"
    +        },
    +        "body": {
    +          "clientId": "1234567890",
    +          "loanAmount": 99999
    +        },
    +        "generators": {
    +          "body": {
    +            "$.clientId": {
    +              "type": "Regex",
    +              "regex": "[0-9]{10}"
    +            }
    +          }
    +        },
    +        "matchingRules": {
    +          "header": {
    +            "Content-Type": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "application/vnd\\.fraud\\.v1\\+json.*"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          },
    +          "body": {
    +            "$.clientId": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "[0-9]{10}"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          }
    +        }
    +      },
    +      "response": {
    +        "status": 200,
    +        "headers": {
    +          "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8"
    +        },
    +        "body": {
    +          "fraudCheckStatus": "FRAUD",
    +          "rejectionReason": "Amount too high"
    +        },
    +        "matchingRules": {
    +          "header": {
    +            "Content-Type": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "application/vnd\\.fraud\\.v1\\+json.*"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          },
    +          "body": {
    +            "$.fraudCheckStatus": {
    +              "matchers": [
    +                {
    +                  "match": "regex",
    +                  "regex": "FRAUD"
    +                }
    +              ],
    +              "combine": "AND"
    +            }
    +          }
    +        }
    +      }
    +    }
    +  ],
    +  "metadata": {
    +    "pact-specification": {
    +      "version": "3.0.0"
    +    },
    +    "pact-jvm": {
    +      "version": "3.5.13"
    +    }
    +  }
    +}

    The remainder of this section about using Pact refers to the preceding file.

    96.1.3 Pact for Producers

    On the producer side, you must add two additional dependencies to your plugin +configuration. One is the Spring Cloud Contract Pact support, and the other represents +the current Pact version that you use.

    Maven.  +

    <plugin>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
    +	<version>${spring-cloud-contract.version}</version>
    +	<extensions>true</extensions>
    +	<configuration>
    +		<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
    +	</configuration>
    +	<dependencies>
    +		<dependency>
    +			<groupId>org.springframework.cloud</groupId>
    +			<artifactId>spring-cloud-contract-pact</artifactId>
    +			<version>${spring-cloud-contract.version}</version>
    +		</dependency>
    +	</dependencies>
    +</plugin>

    +

    Gradle.  +

    classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}"

    +

    When you execute the build of your application, a test will be generated. The generated +test might be as follows:

    @Test
    +public void validate_shouldMarkClientAsFraud() throws Exception {
    +	// given:
    +		MockMvcRequestSpecification request = given()
    +				.header("Content-Type", "application/vnd.fraud.v1+json")
    +				.body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}");
    +
    +	// when:
    +		ResponseOptions response = given().spec(request)
    +				.put("/fraudcheck");
    +
    +	// then:
    +		assertThat(response.statusCode()).isEqualTo(200);
    +		assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*");
    +	// and:
    +		DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
    +		assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high");
    +	// and:
    +		assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD");
    +}

    The corresponding generated stub might be as follows:

    {
    +  "id" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
    +  "uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62",
    +  "request" : {
    +    "url" : "/fraudcheck",
    +    "method" : "PUT",
    +    "headers" : {
    +      "Content-Type" : {
    +        "matches" : "application/vnd\\.fraud\\.v1\\+json.*"
    +      }
    +    },
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]"
    +    }, {
    +      "matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]"
    +    } ]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}",
    +    "headers" : {
    +      "Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8"
    +    },
    +    "transformers" : [ "response-template" ]
    +  },
    +}

    96.1.4 Pact for Consumers

    On the consumer side, you must add two additional dependencies to your project +dependencies. One is the Spring Cloud Contract Pact support, and the other represents the +current Pact version that you use.

    Maven.  +

    <dependency>
    +	<groupId>org.springframework.cloud</groupId>
    +	<artifactId>spring-cloud-contract-pact</artifactId>
    +	<scope>test</scope>
    +</dependency>

    +

    Gradle.  +

    testCompile "org.springframework.cloud:spring-cloud-contract-pact"

    +

    96.2 Using the Custom Test Generator

    If you want to generate tests for languages other than Java or you are not happy with the +way the verifier builds Java tests, you can register your own implementation.

    The SingleTestGenerator interface lets you register your own implementation. The +following code listing shows the SingleTestGenerator interface:

    package org.springframework.cloud.contract.verifier.builder
    +
    +
    +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
    +import org.springframework.cloud.contract.verifier.file.ContractMetadata
    +
    +/**
    + * Builds a single test.
    + *
    + * @since 1.1.0
    + */
    +trait SingleTestGenerator {
    +
    +	/**
    +	 * Creates contents of a single test class in which all test scenarios from
    +	 * the contract metadata should be placed.
    +	 *
    +	 * @param properties - properties passed to the plugin
    +	 * @param listOfFiles - list of parsed contracts with additional metadata
    +	 * @param className - the name of the generated test class
    +	 * @param classPackage - the name of the package in which the test class should be stored
    +	 * @param includedDirectoryRelativePath - relative path to the included directory
    +	 * @return contents of a single test class
    +	 * @deprecated use{@link SingleTestGenerator#buildClass(ContractVerifierConfigProperties, Collection, String, GeneratedClassData)}
    +	 */
    +	@Deprecated
    +	abstract String buildClass(ContractVerifierConfigProperties properties,
    +			Collection<ContractMetadata> listOfFiles, String className, String classPackage, String includedDirectoryRelativePath)
    +
    +	/**
    +	 * Creates contents of a single test class in which all test scenarios from
    +	 * the contract metadata should be placed.
    +	 *
    +	 * @param properties - properties passed to the plugin
    +	 * @param listOfFiles - list of parsed contracts with additional metadata
    +	 * @param generatedClassData - information about the generated class
    +	 * @param includedDirectoryRelativePath - relative path to the included directory
    +	 * @return contents of a single test class
    +	 */
    +	String buildClass(ContractVerifierConfigProperties properties,
    +			Collection<ContractMetadata> listOfFiles, String includedDirectoryRelativePath, GeneratedClassData generatedClassData) {
    +		String className = generatedClassData.className
    +		String classPackage = generatedClassData.classPackage
    +		String path = includedDirectoryRelativePath
    +		return buildClass(properties, listOfFiles, className, classPackage, path)
    +	}
    +
    +	/**
    +	 * Extension that should be appended to the generated test class. E.g. {@code .java} or {@code .php}
    +	 *
    +	 * @param properties - properties passed to the plugin
    +	 */
    +	abstract String fileExtension(ContractVerifierConfigProperties properties)
    +
    +	static class GeneratedClassData {
    +		public final String className
    +		public final String classPackage
    +		public final java.nio.file.Path testClassPath
    +
    +		GeneratedClassData(String className, String classPackage,
    +				java.nio.file.Path testClassPath) {
    +			this.className = className
    +			this.classPackage = classPackage
    +			this.testClassPath = testClassPath
    +		}
    +	}
    +}

    Again, you must provide a spring.factories file, such as the one shown in the following +example:

    org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/
    +com.example.MyGenerator

    96.3 Using the Custom Stub Generator

    If you want to generate stubs for stub servers other than WireMock, you can plug in your +own implementation of the StubGenerator interface. The following code listing shows the +StubGenerator interface:

    package org.springframework.cloud.contract.verifier.converter
    +
    +import groovy.transform.CompileStatic
    +
    +import org.springframework.cloud.contract.spec.Contract
    +import org.springframework.cloud.contract.verifier.file.ContractMetadata
    +
    +/**
    + * Converts contracts into their stub representation.
    + *
    + * @since 1.1.0
    + */
    +@CompileStatic
    +interface StubGenerator {
    +
    +	/**
    +	 * @return {@code true} if the converter can handle the file to convert it into a stub.
    +	 */
    +	boolean canHandleFileName(String fileName)
    +
    +	/**
    +	 * @return the collection of converted contracts into stubs. One contract can
    +	 * result in multiple stubs.
    +	 */
    +	Map<Contract, String> convertContents(String rootName, ContractMetadata content)
    +
    +	/**
    +	 * @return the name of the converted stub file. If you have multiple contracts
    +	 * in a single file then a prefix will be added to the generated file. If you
    +	 * provide the {@link Contract#name} field then that field will override the
    +	 * generated file name.
    +	 *
    +	 * Example: name of file with 2 contracts is {@code foo.groovy}, it will be
    +	 * converted by the implementation to {@code foo.json}. The recursive file
    +	 * converter will create two files {@code 0_foo.json} and {@code 1_foo.json}
    +	 */
    +	String generateOutputFileNameForInput(String inputFileName)
    +}

    Again, you must provide a spring.factories file, such as the one shown in the following +example:

    # Stub converters
    +org.springframework.cloud.contract.verifier.converter.StubGenerator=\
    +org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter

    The default implementation is the WireMock stub generation.

    [Tip]Tip

    You can provide multiple stub generator implementations. For example, from a single +DSL, you can produce both WireMock stubs and Pact files.

    96.4 Using the Custom Stub Runner

    If you decide to use a custom stub generation, you also need a custom way of running +stubs with your different stub provider.

    Assume that you use Moco to build your stubs and that +you have written a stub generator and placed your stubs in a JAR file.

    In order for Stub Runner to know how to run your stubs, you have to define a custom +HTTP Stub server implementation, which might resemble the following example:

    package org.springframework.cloud.contract.stubrunner.provider.moco
    +
    +import com.github.dreamhead.moco.bootstrap.arg.HttpArgs
    +import com.github.dreamhead.moco.runner.JsonRunner
    +import com.github.dreamhead.moco.runner.RunnerSetting
    +import groovy.util.logging.Commons
    +
    +import org.springframework.cloud.contract.stubrunner.HttpServerStub
    +import org.springframework.util.SocketUtils
    +
    +@Commons
    +class MocoHttpServerStub implements HttpServerStub {
    +
    +	private boolean started
    +	private JsonRunner runner
    +	private int port
    +
    +	@Override
    +	int port() {
    +		if (!isRunning()) {
    +			return -1
    +		}
    +		return port
    +	}
    +
    +	@Override
    +	boolean isRunning() {
    +		return started
    +	}
    +
    +	@Override
    +	HttpServerStub start() {
    +		return start(SocketUtils.findAvailableTcpPort())
    +	}
    +
    +	@Override
    +	HttpServerStub start(int port) {
    +		this.port = port
    +		return this
    +	}
    +
    +	@Override
    +	HttpServerStub stop() {
    +		if (!isRunning()) {
    +			return this
    +		}
    +		this.runner.stop()
    +		return this
    +	}
    +
    +	@Override
    +	HttpServerStub registerMappings(Collection<File> stubFiles) {
    +		List<RunnerSetting> settings = stubFiles.findAll { it.name.endsWith("json") }
    +			.collect {
    +			log.info("Trying to parse [${it.name}]")
    +			try {
    +				return RunnerSetting.aRunnerSetting().withStream(it.newInputStream()).
    +					build()
    +			}
    +			catch (Exception e) {
    +				log.warn("Exception occurred while trying to parse file [${it.name}]", e)
    +				return null
    +			}
    +		}.findAll { it }
    +		this.runner = JsonRunner.newJsonRunnerWithSetting(settings,
    +			HttpArgs.httpArgs().withPort(this.port).build())
    +		this.runner.run()
    +		this.started = true
    +		return this
    +	}
    +
    +	@Override
    +	String registeredMappings() {
    +		return ""
    +	}
    +
    +	@Override
    +	boolean isAccepted(File file) {
    +		return file.name.endsWith(".json")
    +	}
    +}

    Then, you can register it in your spring.factories file, as shown in the following +example:

    org.springframework.cloud.contract.stubrunner.HttpServerStub=\
    +org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub

    Now you can run stubs with Moco.

    [Important]Important

    If you do not provide any implementation, then the default (WireMock) +implementation is used. If you provide more than one, the first one on the list is used.

    96.5 Using the Custom Stub Downloader

    You can customize the way your stubs are downloaded by creating an implementation of the +StubDownloaderBuilder interface, as shown in the following example:

    package com.example;
    +
    +class CustomStubDownloaderBuilder implements StubDownloaderBuilder {
    +
    +	@Override
    +	public StubDownloader build(final StubRunnerOptions stubRunnerOptions) {
    +		return new StubDownloader() {
    +			@Override
    +			public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar(
    +					StubConfiguration config) {
    +				File unpackedStubs = retrieveStubs();
    +				return new AbstractMap.SimpleEntry<>(
    +						new StubConfiguration(config.getGroupId(), config.getArtifactId(), version,
    +								config.getClassifier()), unpackedStubs);
    +			}
    +
    +			File retrieveStubs() {
    +			    // here goes your custom logic to provide a folder where all the stubs reside
    +			}
    +}

    Then you can register it in your spring.factories file, as shown in the following +example:

    # Example of a custom Stub Downloader Provider
    +org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\
    +com.example.CustomStubDownloaderBuilder

    Now you can pick a folder with the source of your stubs.

    [Important]Important

    If you do not provide any implementation, then the default is used (scan classpath). +If you provide the stubsMode = StubRunnerProperties.StubsMode.LOCAL or +, stubsMode = StubRunnerProperties.StubsMode.REMOTE then the Aether implementation will be used +If you provide more than one, then the first one on the list is used.

    96.6 Using the SCM Stub Downloader

    Whenever the repositoryRoot starts with a SCM protocol +(currently we support only git://), the stub downloader will try +to clone the repository and use it as a source of contracts +to generate tests or stubs.

    Either via environment variables, system properties, properties set +inside the plugin or contracts repository configuration you can +tweak the downloader’s behaviour. Below you can find the list of +properties

    Table 96.1. SCM Stub Downloader properties

    Type of a property

    Name of the property

    Description

    * git.branch (plugin prop)

    * stubrunner.properties.git.branch (system prop)

    * STUBRUNNER_PROPERTIES_GIT_BRANCH (env prop)

    master

    Which branch to checkout

    * git.username (plugin prop)

    * stubrunner.properties.git.username (system prop)

    * STUBRUNNER_PROPERTIES_GIT_USERNAME (env prop)

     

    Git clone username

    * git.password (plugin prop)

    * stubrunner.properties.git.password (system prop)

    * STUBRUNNER_PROPERTIES_GIT_PASSWORD (env prop)

     

    Git clone password

    * git.no-of-attempts (plugin prop)

    * stubrunner.properties.git.no-of-attempts (system prop)

    * STUBRUNNER_PROPERTIES_GIT_NO_OF_ATTEMPTS (env prop)

    10

    Number of attempts to push the commits to origin

    * git.wait-between-attempts (Plugin prop)

    * stubrunner.properties.git.wait-between-attempts (system prop)

    * STUBRUNNER_PROPERTIES_GIT_WAIT_BETWEEN_ATTEMPTS (env prop)

    1000

    Number of millis to wait between attempts to push the commits to origin


    96.7 Using the Pact Stub Downloader

    Whenever the repositoryRoot starts with a Pact protocol +(starts with pact://), the stub downloader will try +to fetch the Pact contract definitions from the Pact Broker. +Whatever is set after pact:// will be parsed as the Pact Broker URL.

    Either via environment variables, system properties, properties set +inside the plugin or contracts repository configuration you can +tweak the downloader’s behaviour. Below you can find the list of +properties

    Table 96.2. SCM Stub Downloader properties

    Name of a property

    Default

    Description

    * pactbroker.host (plugin prop)

    * stubrunner.properties.pactbroker.host (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_HOST (env prop)

    Host from URL passed to repositoryRoot

    What is the URL of Pact Broker

    * pactbroker.port (plugin prop)

    * stubrunner.properties.pactbroker.port (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_PORT (env prop)

    Port from URL passed to repositoryRoot

    What is the port of Pact Broker

    * pactbroker.protocol (plugin prop)

    * stubrunner.properties.pactbroker.protocol (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_PROTOCOL (env prop)

    Protocol from URL passed to repositoryRoot

    What is the protocol of Pact Broker

    * pactbroker.tags (plugin prop)

    * stubrunner.properties.pactbroker.tags (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_TAGS (env prop)

    Version of the stub, or latest if version is +

    What tags should be used to fetch the stub

    * pactbroker.auth.scheme (plugin prop)

    * stubrunner.properties.pactbroker.auth.scheme (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_SCHEME (env prop)

    Basic

    What kind of authentication should be used to connect to the Pact Broker

    * pactbroker.auth.username (plugin prop)

    * stubrunner.properties.pactbroker.auth.username (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_USERNAME (env prop)

    The username passed to contractsRepositoryUsername (maven) or contractRepository.username (gradle)

    Username used to connect to the Pact Broker

    * pactbroker.auth.password (plugin prop)

    * stubrunner.properties.pactbroker.auth.password (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD (env prop)

    The password passed to contractsRepositoryPassword (maven) or contractRepository.password (gradle)

    Password used to connect to the Pact Broker

    * pactbroker.provider-name-with-group-id (plugin prop)

    * stubrunner.properties.pactbroker.provider-name-with-group-id (system prop)

    * STUBRUNNER_PROPERTIES_PACTBROKER_PROVIDER_NAME_WITH_GROUP_ID (env prop)

    false

    When true, the provider name will be a combination of groupId:artifactId. If false, just artifactId is used


    97. Spring Cloud Contract WireMock

    The Spring Cloud Contract WireMock modules let you use WireMock in a +Spring Boot application. Check out the +samples +for more details.

    If you have a Spring Boot application that uses Tomcat as an embedded server (which is +the default with spring-boot-starter-web), you can add +spring-cloud-starter-contract-stub-runner to your classpath and add @AutoConfigureWireMock in +order to be able to use Wiremock in your tests. Wiremock runs as a stub server and you +can register stub behavior using a Java API or via static JSON declarations as part of +your test. The following code shows an example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
    +@AutoConfigureWireMock(port = 0)
    +public class WiremockForDocsTests {
    +
    +	// A service that calls out over HTTP
    +	@Autowired
    +	private Service service;
    +
    +	@Before
    +	public void setup() {
    +		this.service.setBase("http://localhost:"
    +				+ this.environment.getProperty("wiremock.server.port"));
    +	}
    +
    +	// Using the WireMock APIs in the normal way:
    +	@Test
    +	public void contextLoads() throws Exception {
    +		// Stubbing WireMock
    +		stubFor(get(urlEqualTo("/resource")).willReturn(aResponse()
    +				.withHeader("Content-Type", "text/plain").withBody("Hello World!")));
    +		// We're asserting if WireMock responded properly
    +		assertThat(this.service.go()).isEqualTo("Hello World!");
    +	}
    +
    +}

    To start the stub server on a different port use (for example), +@AutoConfigureWireMock(port=9999). For a random port, use a value of 0. The stub +server port can be bound in the test application context with the "wiremock.server.port" +property. Using @AutoConfigureWireMock adds a bean of type WiremockConfiguration to +your test application context, where it will be cached in between methods and classes +having the same context, the same as for Spring integration tests. Also you can inject a bean of type WireMockServer into your test.

    97.1 Registering Stubs Automatically

    If you use @AutoConfigureWireMock, it registers WireMock JSON stubs from the file +system or classpath (by default, from file:src/test/resources/mappings). You can +customize the locations using the stubs attribute in the annotation, which can be an +Ant-style resource pattern or a directory. In the case of a directory, */.json is +appended. The following code shows an example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureWireMock(stubs="classpath:/stubs")
    +public class WiremockImportApplicationTests {
    +
    +	@Autowired
    +	private Service service;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		assertThat(this.service.go()).isEqualTo("Hello World!");
    +	}
    +
    +}
    [Note]Note

    Actually, WireMock always loads mappings from src/test/resources/mappings as +well as the custom locations in the stubs attribute. To change this behavior, you can +also specify a files root as described in the next section of this document.

    If you’re using Spring Cloud Contract’s default stub jars, then your +stubs are stored under /META-INF/group-id/artifact-id/versions/mappings/ folder. If you want to register all stubs from that location, from all embedded JARs, then it’s enough to use the following syntax.

    @AutoConfigureWireMock(port = 0, stubs = "classpath*:/META-INF/**/mappings/**/*.json")

    97.2 Using Files to Specify the Stub Bodies

    WireMock can read response bodies from files on the classpath or the file system. In that +case, you can see in the JSON DSL that the response has a bodyFileName instead of a +(literal) body. The files are resolved relative to a root directory (by default, +src/test/resources/__files). To customize this location you can set the files +attribute in the @AutoConfigureWireMock annotation to the location of the parent +directory (in other words, __files is a subdirectory). You can use Spring resource +notation to refer to file:…​ or classpath:…​ locations. Generic URLs are not +supported. A list of values can be given, in which case WireMock resolves the first file +that exists when it needs to find a response body.

    [Note]Note

    When you configure the files root, it also affects the +automatic loading of stubs, because they come from the root location +in a subdirectory called "mappings". The value of files has no +effect on the stubs loaded explicitly from the stubs attribute.

    97.3 Alternative: Using JUnit Rules

    For a more conventional WireMock experience, you can use JUnit @Rules to start and stop +the server. To do so, use the WireMockSpring convenience class to obtain an Options +instance, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
    +public class WiremockForDocsClassRuleTests {
    +
    +	// Start WireMock on some dynamic port
    +	// for some reason `dynamicPort()` is not working properly
    +	@ClassRule
    +	public static WireMockClassRule wiremock = new WireMockClassRule(
    +			WireMockSpring.options().dynamicPort());
    +
    +	// A service that calls out over HTTP to wiremock's port
    +	@Autowired
    +	private Service service;
    +
    +	@Before
    +	public void setup() {
    +		this.service.setBase("http://localhost:" + wiremock.port());
    +	}
    +
    +	// Using the WireMock APIs in the normal way:
    +	@Test
    +	public void contextLoads() throws Exception {
    +		// Stubbing WireMock
    +		wiremock.stubFor(get(urlEqualTo("/resource")).willReturn(aResponse()
    +				.withHeader("Content-Type", "text/plain").withBody("Hello World!")));
    +		// We're asserting if WireMock responded properly
    +		assertThat(this.service.go()).isEqualTo("Hello World!");
    +	}
    +
    +}

    The @ClassRule means that the server shuts down after all the methods in this class +have been run.

    97.4 Relaxed SSL Validation for Rest Template

    WireMock lets you stub a "secure" server with an "https" URL protocol. If your +application wants to contact that stub server in an integration test, it will find that +the SSL certificates are not valid (the usual problem with self-installed certificates). +The best option is often to re-configure the client to use "http". If that’s not an +option, you can ask Spring to configure an HTTP client that ignores SSL validation errors +(do so only for tests, of course).

    To make this work with minimum fuss, you need to be using the Spring Boot +RestTemplateBuilder in your app, as shown in the following example:

    @Bean
    +public RestTemplate restTemplate(RestTemplateBuilder builder) {
    +	return builder.build();
    +}

    You need RestTemplateBuilder because the builder is passed through callbacks to +initialize it, so the SSL validation can be set up in the client at that point. This +happens automatically in your test if you are using the @AutoConfigureWireMock +annotation or the stub runner. If you use the JUnit @Rule approach, you need to add the +@AutoConfigureHttpClient annotation as well, as shown in the following example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest("app.baseUrl=https://localhost:6443")
    +@AutoConfigureHttpClient
    +public class WiremockHttpsServerApplicationTests {
    +
    +	@ClassRule
    +	public static WireMockClassRule wiremock = new WireMockClassRule(
    +			WireMockSpring.options().httpsPort(6443));
    +...
    +}

    If you are using spring-boot-starter-test, you have the Apache HTTP client on the +classpath and it is selected by the RestTemplateBuilder and configured to ignore SSL +errors. If you use the default java.net client, you do not need the annotation (but it +won’t do any harm). There is no support currently for other clients, but it may be added +in future releases.

    To disable the custom RestTemplateBuilder, set the wiremock.rest-template-ssl-enabled +property to false.

    97.5 WireMock and Spring MVC Mocks

    Spring Cloud Contract provides a convenience class that can load JSON WireMock stubs into +a Spring MockRestServiceServer. The following code shows an example:

    @RunWith(SpringRunner.class)
    +@SpringBootTest(webEnvironment = WebEnvironment.NONE)
    +public class WiremockForDocsMockServerApplicationTests {
    +
    +	@Autowired
    +	private RestTemplate restTemplate;
    +
    +	@Autowired
    +	private Service service;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		// will read stubs classpath
    +		MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate)
    +				.baseUrl("https://example.org").stubs("classpath:/stubs/resource.json")
    +				.build();
    +		// We're asserting if WireMock responded properly
    +		assertThat(this.service.go()).isEqualTo("Hello World");
    +		server.verify();
    +	}
    +
    +}

    The baseUrl value is prepended to all mock calls, and the stubs() method takes a stub +path resource pattern as an argument. In the preceding example, the stub defined at +/stubs/resource.json is loaded into the mock server. If the RestTemplate is asked to +visit https://example.org/, it gets the responses as being declared at that URL. More +than one stub pattern can be specified, and each one can be a directory (for a recursive +list of all ".json"), a fixed filename (as in the example above), or an Ant-style +pattern. The JSON format is the normal WireMock format, which you can read about in the +WireMock website.

    Currently, the Spring Cloud Contract Verifier supports Tomcat, Jetty, and Undertow as +Spring Boot embedded servers, and Wiremock itself has "native" support for a particular +version of Jetty (currently 9.2). To use the native Jetty, you need to add the native +Wiremock dependencies and exclude the Spring Boot container (if there is one).

    97.6 Customization of WireMock configuration

    You can register a bean of org.springframework.cloud.contract.wiremock.WireMockConfigurationCustomizer type +in order to customize the WireMock configuration (e.g. add custom transformers). +Example:

    		@Bean
    +		WireMockConfigurationCustomizer optionsCustomizer() {
    +			return new WireMockConfigurationCustomizer() {
    +				@Override
    +				public void customize(WireMockConfiguration options) {
    +// perform your customization here
    +				}
    +			};
    +		}

    97.7 Generating Stubs using REST Docs

    Spring REST Docs can be used to generate +documentation (for example in Asciidoctor format) for an HTTP API with Spring MockMvc +or WebTestClient or Rest Assured. At the same time that you generate documentation for your API, you can also +generate WireMock stubs by using Spring Cloud Contract WireMock. To do so, write your +normal REST Docs test cases and use @AutoConfigureRestDocs to have stubs be +automatically generated in the REST Docs output directory. The following code shows an +example using MockMvc:

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureRestDocs(outputDir = "target/snippets")
    +@AutoConfigureMockMvc
    +public class ApplicationTests {
    +
    +	@Autowired
    +	private MockMvc mockMvc;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		mockMvc.perform(get("/resource"))
    +				.andExpect(content().string("Hello World"))
    +				.andDo(document("resource"));
    +	}
    +}

    This test generates a WireMock stub at "target/snippets/stubs/resource.json". It matches +all GET requests to the "/resource" path. The same example with WebTestClient (used +for testing Spring WebFlux applications) would look like this:

    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureRestDocs(outputDir = "target/snippets")
    +@AutoConfigureWebTestClient
    +public class ApplicationTests {
    +
    +	@Autowired
    +	private WebTestClient client;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		client.get().uri("/resource").exchange()
    +				.expectBody(String.class).isEqualTo("Hello World")
    + 				.consumeWith(document("resource"));
    +	}
    +}

    Without any additional configuration, these tests create a stub with a request matcher +for the HTTP method and all headers except "host" and "content-length". To match the +request more precisely (for example, to match the body of a POST or PUT), we need to +explicitly create a request matcher. Doing so has two effects:

    • Creating a stub that matches only in the way you specify.
    • Asserting that the request in the test case also matches the same conditions.

    The main entry point for this feature is WireMockRestDocs.verify(), which can be used +as a substitute for the document() convenience method, as shown in the following +example:

    import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify;
    @RunWith(SpringRunner.class)
    +@SpringBootTest
    +@AutoConfigureRestDocs(outputDir = "target/snippets")
    +@AutoConfigureMockMvc
    +public class ApplicationTests {
    +
    +	@Autowired
    +	private MockMvc mockMvc;
    +
    +	@Test
    +	public void contextLoads() throws Exception {
    +		mockMvc.perform(post("/resource")
    +                .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
    +				.andExpect(status().isOk())
    +				.andDo(verify().jsonPath("$.id"))
    +                .andDo(document("resource"));
    +	}
    +}

    This contract specifies that any valid POST with an "id" field receives the response +defined in this test. You can chain together calls to .jsonPath() to add additional +matchers. If JSON Path is unfamiliar, The JayWay +documentation can help you get up to speed. The WebTestClient version of this test +has a similar verify() static helper that you insert in the same place.

    Instead of the jsonPath and contentType convenience methods, you can also use the +WireMock APIs to verify that the request matches the created stub, as shown in the +following example:

    @Test
    +public void contextLoads() throws Exception {
    +	mockMvc.perform(post("/resource")
    +               .content("{\"id\":\"123456\",\"message\":\"Hello World\"}"))
    +			.andExpect(status().isOk())
    +			.andDo(verify()
    +					.wiremock(WireMock.post(
    +						urlPathEquals("/resource"))
    +						.withRequestBody(matchingJsonPath("$.id")))
    +                       .andDo(document("post-resource"));
    +}

    The WireMock API is rich. You can match headers, query parameters, and request body by +regex as well as by JSON path. These features can be used to create stubs with a wider +range of parameters. The above example generates a stub resembling the following example:

    post-resource.json.  +

    {
    +  "request" : {
    +    "url" : "/resource",
    +    "method" : "POST",
    +    "bodyPatterns" : [ {
    +      "matchesJsonPath" : "$.id"
    +    }]
    +  },
    +  "response" : {
    +    "status" : 200,
    +    "body" : "Hello World",
    +    "headers" : {
    +      "X-Application-Context" : "application:-1",
    +      "Content-Type" : "text/plain"
    +    }
    +  }
    +}

    +

    [Note]Note

    You can use either the wiremock() method or the jsonPath() and contentType() +methods to create request matchers, but you can’t use both approaches.

    On the consumer side, you can make the resource.json generated earlier in this section +available on the classpath (by +<<publishing-stubs-as-jars], for example). After that, you can create a stub using WireMock in a +number of different ways, including by using +@AutoConfigureWireMock(stubs="classpath:resource.json"), as described earlier in this +document.

    97.8 Generating Contracts by Using REST Docs

    You can also generate Spring Cloud Contract DSL files and documentation with Spring REST +Docs. If you do so in combination with Spring Cloud WireMock, you get both the contracts +and the stubs.

    Why would you want to use this feature? Some people in the community asked questions +about a situation in which they would like to move to DSL-based contract definition, +but they already have a lot of Spring MVC tests. Using this feature lets you generate +the contract files that you can later modify and move to folders (defined in your +configuration) so that the plugin finds them.

    [Tip]Tip

    You might wonder why this functionality is in the WireMock module. The functionality +is there because it makes sense to generate both the contracts and the stubs.

    Consider the following test:

    		this.mockMvc
    +				.perform(post("/foo").accept(MediaType.APPLICATION_PDF)
    +						.accept(MediaType.APPLICATION_JSON)
    +						.contentType(MediaType.APPLICATION_JSON)
    +						.content("{\"foo\": 23, \"bar\" : \"baz\" }"))
    +				.andExpect(status().isOk()).andExpect(content().string("bar"))
    +				// first WireMock
    +				.andDo(WireMockRestDocs.verify().jsonPath("$[?(@.foo >= 20)]")
    +						.jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]")
    +						.contentType(MediaType.valueOf("application/json")))
    +				// then Contract DSL documentation
    +				.andDo(document("index", SpringCloudContractRestDocs.dslContract()));

    The preceding test creates the stub presented in the previous section, generating both +the contract and a documentation file.

    The contract is called index.groovy and might look like the following example:

    import org.springframework.cloud.contract.spec.Contract
    +
    +Contract.make {
    +    request {
    +        method 'POST'
    +        url '/foo'
    +        body('''
    +            {"foo": 23 }
    +        ''')
    +        headers {
    +            header('''Accept''', '''application/json''')
    +            header('''Content-Type''', '''application/json''')
    +        }
    +    }
    +    response {
    +        status OK()
    +        body('''
    +        bar
    +        ''')
    +        headers {
    +            header('''Content-Type''', '''application/json;charset=UTF-8''')
    +            header('''Content-Length''', '''3''')
    +        }
    +        testMatchers {
    +            jsonPath('$[?(@.foo >= 20)]', byType())
    +        }
    +    }
    +}

    The generated document (formatted in Asciidoc in this case) contains a formatted +contract. The location of this file would be index/dsl-contract.adoc.

    98. Migrations

    [Tip]Tip

    For up to date migration guides please visit +the project’s wiki page.

    This section covers migrating from one version of Spring Cloud Contract Verifier to the +next version. It covers the following versions upgrade paths:

    98.1 1.0.x → 1.1.x

    This section covers upgrading from version 1.0 to version 1.1.

    98.1.1 New structure of generated stubs

    In 1.1.x we have introduced a change to the structure of generated stubs. If you have +been using the @AutoConfigureWireMock notation to use the stubs from the classpath, +it no longer works. The following example shows how the @AutoConfigureWireMock notation +used to work:

    @AutoConfigureWireMock(stubs = "classpath:/customer-stubs/mappings", port = 8084)

    You must either change the location of the stubs to: +classpath:…​/META-INF/groupId/artifactId/version/mappings or use the new +classpath-based @AutoConfigureStubRunner, as shown in the following example:

    @AutoConfigureWireMock(stubs = "classpath:customer-stubs/META-INF/travel.components/customer-contract/1.0.2-SNAPSHOT/mappings/", port = 8084)

    If you do not want to use @AutoConfigureStubRunner and you want to remain with the old +structure, set your plugin tasks accordingly. The following example would work for the +structure presented in the previous snippet.

    Maven.  +

    <!-- start of pom.xml -->
    +
    +<properties>
    +    <!-- we don't want the verifier to do a jar for us -->
    +    <spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip>
    +</properties>
    +
    +<!-- ... -->
    +
    +<!-- You need to set up the assembly plugin -->
    +<build>
    +    <plugins>
    +        <plugin>
    +            <groupId>org.apache.maven.plugins</groupId>
    +            <artifactId>maven-assembly-plugin</artifactId>
    +            <executions>
    +                <execution>
    +                    <id>stub</id>
    +                    <phase>prepare-package</phase>
    +                    <goals>
    +                        <goal>single</goal>
    +                    </goals>
    +                    <inherited>false</inherited>
    +                    <configuration>
    +                        <attach>true</attach>
    +                        <descriptor>$../../../../src/assembly/stub.xml</descriptor>
    +                    </configuration>
    +                </execution>
    +            </executions>
    +        </plugin>
    +    </plugins>
    +</build>
    +<!-- end of pom.xml -->
    +
    +<!-- start of stub.xml-->
    +
    +<assembly
    +	xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
    +	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd">
    +	<id>stubs</id>
    +	<formats>
    +		<format>jar</format>
    +	</formats>
    +	<includeBaseDirectory>false</includeBaseDirectory>
    +	<fileSets>
    +		<fileSet>
    +			<directory>${project.build.directory}/snippets/stubs</directory>
    +			<outputDirectory>customer-stubs/mappings</outputDirectory>
    +			<includes>
    +				<include>**/*</include>
    +			</includes>
    +		</fileSet>
    +		<fileSet>
    +			<directory>$../../../../src/test/resources/contracts</directory>
    +			<outputDirectory>customer-stubs/contracts</outputDirectory>
    +			<includes>
    +				<include>**/*.groovy</include>
    +			</includes>
    +		</fileSet>
    +	</fileSets>
    +</assembly>
    +
    +<!-- end of stub.xml-->

    +

    Gradle.  +

    task copyStubs(type: Copy, dependsOn: 'generateWireMockClientStubs') {
    +//    Preserve directory structure from 1.0.X of spring-cloud-contract
    +    from "${project.buildDir}/resources/main/customer-stubs/META-INF/${project.group}/${project.name}/${project.version}"
    +    into "${project.buildDir}/resources/main/customer-stubs"
    +}

    +

    98.2 1.1.x → 1.2.x

    This section covers upgrading from version 1.1 to version 1.2.

    98.2.1 Custom HttpServerStub

    HttpServerStub includes a method that was not in version 1.1. The method is +String registeredMappings() If you have classes that implement HttpServerStub, you +now have to implement the registeredMappings() method. It should return a String +representing all mappings available in a single HttpServerStub.

    See issue 355 for more +detail.

    98.2.2 New packages for generated tests

    The flow for setting the generated tests package name will look like this:

    • Set basePackageForTests
    • If basePackageForTests was not set, pick the package from baseClassForTests
    • If baseClassForTests was not set, pick packageWithBaseClasses
    • If nothing got set, pick the default value: +org.springframework.cloud.contract.verifier.tests

    See issue 260 for more +detail.

    98.2.3 New Methods in TemplateProcessor

    In order to add support for fromRequest.path, the following methods had to be added to the +TemplateProcessor interface:

    • path()
    • path(int index)

    See issue 388 for more +detail.

    98.2.4 RestAssured 3.0

    Rest Assured, used in the generated test classes, got bumped to 3.0. If +you manually set versions of Spring Cloud Contract and the release train +you might see the following exception:

    Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile (default-testCompile) on project some-project: Compilation failure: Compilation failure:
    +[ERROR] /some/path/SomeClass.java:[4,39] package com.jayway.restassured.response does not exist

    This exception will occur due to the fact that the tests got generated with +an old version of plugin and at test execution time you have an incompatible +version of the release train (and vice versa).

    Done via issue 267

    98.3 1.2.x → 2.0.x

    Part XIV. Spring Cloud Vault

    © 2016-2019 The original authors.

    [Note]Note

    Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

    Spring Cloud Vault Config provides client-side support for externalized configuration in a distributed system. With HashiCorp’s Vault you have a central place to manage external secret properties for applications across all environments. Vault can manage static and dynamic secrets such as username/password for remote applications/resources and provide credentials for external services such as MySQL, PostgreSQL, Apache Cassandra, MongoDB, Consul, AWS and more.

    100. Quick Start

    Prerequisites

    To get started with Vault and this guide you need a +*NIX-like operating systems that provides:

    • wget, openssl and unzip
    • at least Java 7 and a properly configured JAVA_HOME environment variable

    Install Vault

    $ src/test/bash/install_vault.sh

    Create SSL certificates for Vault

    $ src/test/bash/create_certificates.sh
    [Note]Note

    create_certificates.sh creates certificates in work/ca and a JKS truststore work/keystore.jks. If you want to run Spring Cloud Vault using this quickstart guide you need to configure the truststore the spring.cloud.vault.ssl.trust-store property to file:work/keystore.jks.

    Start Vault server

    $ src/test/bash/local_run_vault.sh

    Vault is started listening on 0.0.0.0:8200 using the inmem storage and +https. +Vault is sealed and not initialized when starting up.

    [Note]Note

    If you want to run tests, leave Vault uninitialized. The tests will +initialize Vault and create a root token 00000000-0000-0000-0000-000000000000.

    If you want to use Vault for your application or give it a try then you need to initialize it first.

    $ export VAULT_ADDR="https://localhost:8200"
    +$ export VAULT_SKIP_VERIFY=true # Don't do this for production
    +$ vault init

    You should see something like:

    Key 1: 7149c6a2e16b8833f6eb1e76df03e47f6113a3288b3093faf5033d44f0e70fe701
    +Key 2: 901c534c7988c18c20435a85213c683bdcf0efcd82e38e2893779f152978c18c02
    +Key 3: 03ff3948575b1165a20c20ee7c3e6edf04f4cdbe0e82dbff5be49c63f98bc03a03
    +Key 4: 216ae5cc3ddaf93ceb8e1d15bb9fc3176653f5b738f5f3d1ee00cd7dccbe926e04
    +Key 5: b2898fc8130929d569c1677ee69dc5f3be57d7c4b494a6062693ce0b1c4d93d805
    +Initial Root Token: 19aefa97-cccc-bbbb-aaaa-225940e63d76
    +
    +Vault initialized with 5 keys and a key threshold of 3. Please
    +securely distribute the above keys. When the Vault is re-sealed,
    +restarted, or stopped, you must provide at least 3 of these keys
    +to unseal it again.
    +
    +Vault does not store the master key. Without at least 3 keys,
    +your Vault will remain permanently sealed.

    Vault will initialize and return a set of unsealing keys and the root token. +Pick 3 keys and unseal Vault. Store the Vault token in the VAULT_TOKEN + environment variable.

    $ vault unseal (Key 1)
    +$ vault unseal (Key 2)
    +$ vault unseal (Key 3)
    +$ export VAULT_TOKEN=(Root token)
    +# Required to run Spring Cloud Vault tests after manual initialization
    +$ vault token-create -id="00000000-0000-0000-0000-000000000000" -policy="root"

    Spring Cloud Vault accesses different resources. By default, the secret +backend is enabled which accesses secret config settings via JSON endpoints.

    The HTTP service has resources in the form:

    /secret/{application}/{profile}
    +/secret/{application}
    +/secret/{defaultContext}/{profile}
    +/secret/{defaultContext}

    where the "application" is injected as the spring.application.name in the +SpringApplication (i.e. what is normally "application" in a regular +Spring Boot app), "profile" is an active profile (or comma-separated +list of properties). Properties retrieved from Vault will be used "as-is" +without further prefixing of the property names.

    101. Client Side Usage

    To use these features in an application, just build it as a Spring +Boot application that depends on spring-cloud-vault-config (e.g. see +the test cases). Example Maven configuration:

    Example 101.1. pom.xml

    <parent>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-parent</artifactId>
    +    <version>2.0.0.RELEASE</version>
    +    <relativePath /> <!-- lookup parent from repository -->
    +</parent>
    +
    +<dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-starter-vault-config</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +    <dependency>
    +        <groupId>org.springframework.boot</groupId>
    +        <artifactId>spring-boot-starter-test</artifactId>
    +        <scope>test</scope>
    +    </dependency>
    +</dependencies>
    +
    +<build>
    +    <plugins>
    +        <plugin>
    +            <groupId>org.springframework.boot</groupId>
    +            <artifactId>spring-boot-maven-plugin</artifactId>
    +        </plugin>
    +    </plugins>
    +</build>
    +
    +<!-- repositories also needed for snapshots and milestones -->

    Then you can create a standard Spring Boot application, like this simple HTTP server:

    @SpringBootApplication
    +@RestController
    +public class Application {
    +
    +    @RequestMapping("/")
    +    public String home() {
    +        return "Hello World!";
    +    }
    +
    +    public static void main(String[] args) {
    +        SpringApplication.run(Application.class, args);
    +    }
    +}

    When it runs it will pick up the external configuration from the +default local Vault server on port 8200 if it is running. To modify +the startup behavior you can change the location of the Vault server +using bootstrap.properties (like application.properties but for +the bootstrap phase of an application context), e.g.

    Example 101.2. bootstrap.yml

    spring.cloud.vault:
    +    host: localhost
    +    port: 8200
    +    scheme: https
    +    uri: https://localhost:8200
    +    connection-timeout: 5000
    +    read-timeout: 15000
    +    config:
    +        order: -10

    • host sets the hostname of the Vault host. The host name will be used +for SSL certificate validation
    • port sets the Vault port
    • scheme setting the scheme to http will use plain HTTP. +Supported schemes are http and https.
    • uri configure the Vault endpoint with an URI. Takes precedence over host/port/scheme configuration
    • connection-timeout sets the connection timeout in milliseconds
    • read-timeout sets the read timeout in milliseconds
    • config.order sets the order for the property source

    Enabling further integrations requires additional dependencies and +configuration. Depending on how you have set up Vault you might need +additional configuration like +SSL and +authentication.

    If the application imports the spring-boot-starter-actuator project, the +status of the vault server will be available via the /health endpoint.

    The vault health indicator can be enabled or disabled through the property management.health.vault.enabled (default to true).

    101.1 Authentication

    Vault requires an authentication mechanism to authorize client requests.

    Spring Cloud Vault supports multiple authentication mechanisms to authenticate applications with Vault.

    For a quickstart, use the root token printed by the Vault initialization.

    Example 101.3. bootstrap.yml

    spring.cloud.vault:
    +    token: 19aefa97-cccc-bbbb-aaaa-225940e63d76

    [Warning]Warning

    Consider carefully your security requirements. Static token authentication is fine if you want quickly get started with Vault, but a static token is not protected any further. Any disclosure to unintended parties allows Vault use with the associated token roles.

    102. Authentication methods

    Different organizations have different requirements for security +and authentication. Vault reflects that need by shipping multiple authentication +methods. Spring Cloud Vault supports token and AppId authentication.

    102.1 Token authentication

    Tokens are the core method for authentication within Vault. +Token authentication requires a static token to be provided using the +Bootstrap Application Context.

    [Note]Note

    Token authentication is the default authentication method. +If a token is disclosed an unintended party gains access to Vault and +can access secrets for the intended client.

    Example 102.1. bootstrap.yml

    spring.cloud.vault:
    +    authentication: TOKEN
    +    token: 00000000-0000-0000-0000-000000000000

    • authentication setting this value to TOKEN selects the Token +authentication method
    • token sets the static token to use

    See also: Vault Documentation: Tokens

    102.2 AppId authentication

    Vault supports AppId +authentication that consists of two hard to guess tokens. The AppId +defaults to spring.application.name that is statically configured. +The second token is the UserId which is a part determined by the application, +usually related to the runtime environment. IP address, Mac address or a +Docker container name are good examples. Spring Cloud Vault Config supports +IP address, Mac address and static UserId’s (e.g. supplied via System properties). +The IP and Mac address are represented as Hex-encoded SHA256 hash.

    IP address-based UserId’s use the local host’s IP address.

    Example 102.2. bootstrap.yml using SHA256 IP-Address UserId’s

    spring.cloud.vault:
    +    authentication: APPID
    +    app-id:
    +        user-id: IP_ADDRESS

    • authentication setting this value to APPID selects the AppId +authentication method
    • app-id-path sets the path of the AppId mount to use
    • user-id sets the UserId method. Possible values are IP_ADDRESS, +MAC_ADDRESS or a class name implementing a custom AppIdUserIdMechanism

    The corresponding command to generate the IP address UserId from a command line is:

    $ echo -n 192.168.99.1 | sha256sum
    [Note]Note

    Including the line break of echo leads to a different hash value +so make sure to include the -n flag.

    Mac address-based UserId’s obtain their network device from the +localhost-bound device. The configuration also allows specifying +a network-interface hint to pick the right device. The value of +network-interface is optional and can be either an interface +name or interface index (0-based).

    Example 102.3. bootstrap.yml using SHA256 Mac-Address UserId’s

    spring.cloud.vault:
    +    authentication: APPID
    +    app-id:
    +        user-id: MAC_ADDRESS
    +        network-interface: eth0

    • network-interface sets network interface to obtain the physical address

    The corresponding command to generate the IP address UserId from a command line is:

    $ echo -n 0AFEDE1234AC | sha256sum
    [Note]Note

    The Mac address is specified uppercase and without colons. +Including the line break of echo leads to a different hash value +so make sure to include the -n flag.

    102.2.1 Custom UserId

    The UserId generation is an open mechanism. You can set +spring.cloud.vault.app-id.user-id to any string and the configured +value will be used as static UserId.

    A more advanced approach lets you set spring.cloud.vault.app-id.user-id to a +classname. This class must be on your classpath and must implement +the org.springframework.cloud.vault.AppIdUserIdMechanism interface +and the createUserId method. Spring Cloud Vault will obtain the UserId +by calling createUserId each time it authenticates using AppId to +obtain a token.

    Example 102.4. bootstrap.yml

    spring.cloud.vault:
    +    authentication: APPID
    +    app-id:
    +        user-id: com.examlple.MyUserIdMechanism

    Example 102.5. MyUserIdMechanism.java

    public class MyUserIdMechanism implements AppIdUserIdMechanism {
    +
    +  @Override
    +  public String createUserId() {
    +    String userId = ...
    +    return userId;
    +  }
    +}

    See also: Vault Documentation: Using the App ID auth backend

    102.3 AppRole authentication

    AppRole is intended for machine +authentication, like the deprecated (since Vault 0.6.1) Section 102.2, “AppId authentication”. +AppRole authentication consists of two hard to guess (secret) tokens: RoleId and SecretId.

    Spring Vault supports various AppRole scenarios (push/pull mode and wrapped).

    RoleId and optionally SecretId must be provided by configuration, +Spring Vault will not look up these or create a custom SecretId.

    Example 102.6. bootstrap.yml with AppRole authentication properties

    spring.cloud.vault:
    +    authentication: APPROLE
    +    app-role:
    +        role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52

    The following scenarios are supported along the required configuration details:

    Table 102.1. Configuration

    Method

    RoleId

    SecretId

    RoleName

    Token

    Provided RoleId/SecretId

    Provided

    Provided

      

    Provided RoleId without SecretId

    Provided

       

    Provided RoleId, Pull SecretId

    Provided

    Provided

    Provided

    Provided

    Pull RoleId, provided SecretId

     

    Provided

    Provided

    Provided

    Full Pull Mode

      

    Provided

    Provided

    Wrapped

       

    Provided

    Wrapped RoleId, provided SecretId

    Provided

      

    Provided

    Provided RoleId, wrapped SecretId

     

    Provided

     

    Provided


    Table 102.2. Pull/Push/Wrapped Matrix

    RoleId

    SecretId

    Supported

    Provided

    Provided

    Provided

    Pull

    Provided

    Wrapped

    Provided

    Absent

    Pull

    Provided

    Pull

    Pull

    Pull

    Wrapped

    Pull

    Absent

    Wrapped

    Provided

    Wrapped

    Pull

    Wrapped

    Wrapped

    Wrapped

    Absent


    [Note]Note

    You can use still all combinations of push/pull/wrapped modes by providing a configured AppRoleAuthentication bean within the bootstrap context. Spring Cloud Vault cannot derive all possible AppRole combinations from the configuration properties.

    [Important]Important

    AppRole authentication is limited to simple pull mode using reactive infrastructure. Full pull mode is not yet supported. Using Spring Cloud Vault with the Spring WebFlux stack enables Vault’s reactive auto-configuration which can be disabled by setting spring.cloud.vault.reactive.enabled=false.

    Example 102.7. bootstrap.yml with all AppRole authentication properties

    spring.cloud.vault:
    +    authentication: APPROLE
    +    app-role:
    +        role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52
    +        secret-id: 1696536f-1976-73b1-b241-0b4213908d39
    +        role: my-role
    +        app-role-path: approle

    • role-id sets the RoleId.
    • secret-id sets the SecretId. SecretId can be omitted if AppRole is configured without requiring SecretId (See bind_secret_id).
    • role: sets the AppRole name for pull mode.
    • app-role-path sets the path of the approle authentication mount to use.

    See also: Vault Documentation: Using the AppRole auth backend

    102.4 AWS-EC2 authentication

    The aws-ec2 +auth backend provides a secure introduction mechanism +for AWS EC2 instances, allowing automated retrieval of a Vault +token. Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats AWS as a Trusted Third Party and uses the +cryptographically signed dynamic metadata information that uniquely +represents each EC2 instance.

    Example 102.8. bootstrap.yml using AWS-EC2 Authentication

    spring.cloud.vault:
    +    authentication: AWS_EC2

    AWS-EC2 authentication enables nonce by default to follow +the Trust On First Use (TOFU) principle. Any unintended party that +gains access to the PKCS#7 identity metadata can authenticate +against Vault.

    During the first login, Spring Cloud Vault generates a nonce +that is stored in the auth backend aside the instance Id. +Re-authentication requires the same nonce to be sent. Any other +party does not have the nonce and can raise an alert in Vault for +further investigation.

    The nonce is kept in memory and is lost during application restart. +You can configure a static nonce with spring.cloud.vault.aws-ec2.nonce.

    AWS-EC2 authentication roles are optional and default to the AMI. +You can configure the authentication role by setting the +spring.cloud.vault.aws-ec2.role property.

    Example 102.9. bootstrap.yml with configured role

    spring.cloud.vault:
    +    authentication: AWS_EC2
    +    aws-ec2:
    +        role: application-server

    Example 102.10. bootstrap.yml with all AWS EC2 authentication properties

    spring.cloud.vault:
    +    authentication: AWS_EC2
    +    aws-ec2:
    +        role: application-server
    +        aws-ec2-path: aws-ec2
    +        identity-document: http://...
    +        nonce: my-static-nonce

    • authentication setting this value to AWS_EC2 selects the AWS EC2 +authentication method
    • role sets the name of the role against which the login is being attempted.
    • aws-ec2-path sets the path of the AWS EC2 mount to use
    • identity-document sets URL of the PKCS#7 AWS EC2 identity document
    • nonce used for AWS-EC2 authentication. An empty nonce defaults to nonce generation

    See also: Vault Documentation: Using the aws auth backend

    102.5 AWS-IAM authentication

    The aws backend provides a secure +authentication mechanism for AWS IAM roles, allowing the automatic authentication with +vault based on the current IAM role of the running application. + Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats AWS as a Trusted Third Party and uses the +4 pieces of information signed by the caller with their IAM credentials + to verify that the caller is indeed using that IAM role.

    The current IAM role the application is running in is automatically calculated. +If you are running your application on AWS ECS then the application +will use the IAM role assigned to the ECS task of the running container. +If you are running your application naked on top of an EC2 instance then +the IAM role used will be the one assigned to the EC2 instance.

    When using the AWS-IAM authentication you must create a role in Vault +and assign it to your IAM role. An empty role defaults to +the friendly name the current IAM role.

    Example 102.11. bootstrap.yml with required AWS-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: AWS_IAM

    Example 102.12. bootstrap.yml with all AWS-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: AWS_IAM
    +    aws-iam:
    +        role: my-dev-role
    +        aws-path: aws
    +        server-id: some.server.name

    • role sets the name of the role against which the login is being attempted. This should be bound to your IAM role. If one is not supplied then the friendly name of the current IAM user will be used as the vault role.
    • aws-path sets the path of the AWS mount to use
    • server-id sets the value to use for the X-Vault-AWS-IAM-Server-ID header preventing certain types of replay attacks.

    AWS-IAM requires the AWS Java SDK dependency (com.amazonaws:aws-java-sdk-core) +as the authentication implementation uses AWS SDK types for credentials and request signing.

    See also: Vault Documentation: Using the aws auth backend

    102.6 Azure MSI authentication

    The azure +auth backend provides a secure introduction mechanism +for Azure VM instances, allowing automated retrieval of a Vault +token. Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats Azure as a Trusted Third Party and uses the +managed service identity and instance metadata information that can be +bound to a VM instance.

    Example 102.13. bootstrap.yml with required Azure Authentication properties

    spring.cloud.vault:
    +    authentication: AZURE_MSI
    +    azure-msi:
    +        role: my-dev-role

    Example 102.14. bootstrap.yml with all Azure Authentication properties

    spring.cloud.vault:
    +    authentication: AZURE_MSI
    +    azure-msi:
    +        role: my-dev-role
    +        azure-path: azure

    • role sets the name of the role against which the login is being attempted.
    • azure-path sets the path of the Azure mount to use

    Azure MSI authentication fetches environmental details about the virtual machine +(subscription Id, resource group, VM name) from the instance metadata service.

    See also: Vault Documentation: Using the azure auth backend

    102.7 TLS certificate authentication

    The cert auth backend allows authentication using SSL/TLS client +certificates that are either signed by a CA or self-signed.

    To enable cert authentication you need to:

    1. Use SSL, see Chapter 108, Vault Client SSL configuration
    2. Configure a Java Keystore that contains the client +certificate and the private key
    3. Set the spring.cloud.vault.authentication to CERT

    Example 102.15. bootstrap.yml

    spring.cloud.vault:
    +    authentication: CERT
    +    ssl:
    +        key-store: classpath:keystore.jks
    +        key-store-password: changeit
    +        cert-auth-path: cert

    See also: Vault Documentation: Using the Cert auth backend

    102.8 Cubbyhole authentication

    Cubbyhole authentication uses Vault primitives to provide a secured authentication +workflow. Cubbyhole authentication uses tokens as primary login method. +An ephemeral token is used to obtain a second, login VaultToken from Vault’s +Cubbyhole secret backend. The login token is usually longer-lived and used to +interact with Vault. The login token will be retrieved from a wrapped +response stored at /cubbyhole/response.

    Creating a wrapped token

    [Note]Note

    Response Wrapping for token creation requires Vault 0.6.0 or higher.

    Example 102.16. Creating and storing tokens

    $ vault token-create -wrap-ttl="10m"
    +Key                            Value
    +---                            -----
    +wrapping_token:                397ccb93-ff6c-b17b-9389-380b01ca2645
    +wrapping_token_ttl:            0h10m0s
    +wrapping_token_creation_time:  2016-09-18 20:29:48.652957077 +0200 CEST
    +wrapped_accessor:              46b6aebb-187f-932a-26d7-4f3d86a68319

    Example 102.17. bootstrap.yml

    spring.cloud.vault:
    +    authentication: CUBBYHOLE
    +    token: 397ccb93-ff6c-b17b-9389-380b01ca2645

    See also:

    102.9 GCP-GCE authentication

    The gcp +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials.

    GCP GCE (Google Compute Engine) authentication creates a signature in the form of a +JSON Web Token (JWT) for a service account. A JWT for a Compute Engine instance +is obtained from the GCE metadata service using Instance identification. +This API creates a JSON Web Token that can be used to confirm the instance identity.

    Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats GCP as a Trusted Third Party and uses the +cryptographically signed dynamic metadata information that uniquely +represents each GCP service account.

    Example 102.18. bootstrap.yml with required GCP-GCE Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_GCE
    +    gcp-gce:
    +        role: my-dev-role

    Example 102.19. bootstrap.yml with all GCP-GCE Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_GCE
    +    gcp-gce:
    +        gcp-path: gcp
    +        role: my-dev-role
    +        service-account: my-service@projectid.iam.gserviceaccount.com

    • role sets the name of the role against which the login is being attempted.
    • gcp-path sets the path of the GCP mount to use
    • service-account allows overriding the service account Id to a specific value. Defaults to the default service account.

    See also:

    102.10 GCP-IAM authentication

    The gcp +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials.

    GCP IAM authentication creates a signature in the form of a JSON Web Token (JWT) +for a service account. A JWT for a service account is obtained by +calling GCP IAM’s projects.serviceAccounts.signJwt API. The caller authenticates against GCP IAM +and proves thereby its identity. This Vault backend treats GCP as a Trusted Third Party.

    IAM credentials can be obtained from either the runtime environment +, specifically the GOOGLE_APPLICATION_CREDENTIALS +environment variable, the Google Compute metadata service, +or supplied externally as e.g. JSON or base64 encoded. +JSON is the preferred form as it carries the project id and +service account identifier required for calling projects.serviceAccounts.signJwt.

    Example 102.20. bootstrap.yml with required GCP-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_IAM
    +    gcp-iam:
    +        role: my-dev-role

    Example 102.21. bootstrap.yml with all GCP-IAM Authentication properties

    spring.cloud.vault:
    +    authentication: GCP_IAM
    +    gcp-iam:
    +        credentials:
    +            location: classpath:credentials.json
    +            encoded-key: e+KApn0=
    +        gcp-path: gcp
    +        jwt-validity: 15m
    +        project-id: my-project-id
    +        role: my-dev-role
    +        service-account-id: my-service@projectid.iam.gserviceaccount.com

    • role sets the name of the role against which the login is being attempted.
    • credentials.location path to the credentials resource that contains Google credentials in JSON format.
    • credentials.encoded-key the base64 encoded contents of an OAuth2 account private key in the JSON format.
    • gcp-path sets the path of the GCP mount to use
    • jwt-validity configures the JWT token validity. Defaults to 15 minutes.
    • project-id allows overriding the project Id to a specific value. Defaults to the project Id from the obtained credential.
    • service-account allows overriding the service account Id to a specific value. Defaults to the service account from the obtained credential.

    GCP IAM authentication requires the Google Cloud Java SDK dependency +(com.google.apis:google-api-services-iam and com.google.auth:google-auth-library-oauth2-http) +as the authentication implementation uses Google APIs for credentials and JWT signing.

    [Note]Note

    Google credentials require an OAuth 2 token maintaining the token lifecycle. All API +is synchronous therefore, GcpIamAuthentication does not support AuthenticationSteps which is +required for reactive usage.

    See also:

    102.11 Kubernetes authentication

    Kubernetes authentication mechanism (since Vault 0.8.3) allows to authenticate with Vault using a Kubernetes Service Account Token. +The authentication is role based and the role is bound to a service account name and a namespace.

    A file containing a JWT token for a pod’s service account is automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

    Example 102.22. bootstrap.yml with all Kubernetes authentication properties

    spring.cloud.vault:
    +    authentication: KUBERNETES
    +    kubernetes:
    +        role: my-dev-role
    +        kubernetes-path: kubernetes
    +        service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token

    • role sets the Role.
    • kubernetes-path sets the path of the Kubernetes mount to use.
    • service-account-token-file sets the location of the file containing the Kubernetes Service Account Token. Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token.

    See also:

    103. Secret Backends

    103.1 Generic Backend

    Spring Cloud Vault supports at the basic level the generic secret +backend. The generic secret backend allows storage of arbitrary +values as key-value store. A single context can store one or many +key-value tuples. Contexts can be organized hierarchically. +Spring Cloud Vault allows using the Application name +and a default context name (application) in combination with active +profiles.

    /secret/{application}/{profile}
    +/secret/{application}
    +/secret/{default-context}/{profile}
    +/secret/{default-context}

    The application name is determined by the properties:

    • spring.cloud.vault.generic.application-name
    • spring.cloud.vault.application-name
    • spring.application.name

    Secrets can be obtained from other contexts within the generic backend by adding their +paths to the application name, separated by commas. For example, given the application +name usefulapp,mysql1,projectx/aws, each of these folders will be used:

    • /secret/usefulapp
    • /secret/mysql1
    • /secret/projectx/aws

    Spring Cloud Vault adds all active profiles to the list of possible context paths. +No active profiles will skip accessing contexts with a profile name.

    Properties are exposed like they are stored (i.e. without additional prefixes).

    spring.cloud.vault:
    +    generic:
    +        enabled: true
    +        backend: secret
    +        profile-separator: '/'
    +        default-context: application
    +        application-name: my-app
    • enabled setting this value to false disables the secret backend +config usage
    • backend sets the path of the secret mount to use
    • default-context sets the context name used by all applications
    • application-name overrides the application name for use in the generic backend
    • profile-separator separates the profile name from the context in +property sources with profiles
    [Note]Note

    The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable generic secret backend usage for non-versioned key-value backends and kv secret backend usage for versioned key-value backends.

    See also: Vault Documentation: Using the KV Secrets Engine - Version 1 (generic secret backend)

    103.2 Versioned Key-Value Backend

    Spring Cloud Vault supports the versioned Key-Value secret +backend. The key-value backend allows storage of arbitrary +values as key-value store. A single context can store one or many +key-value tuples. Contexts can be organized hierarchically. +Spring Cloud Vault allows using the Application name +and a default context name (application) in combination with active +profiles.

    /secret/{application}/{profile}
    +/secret/{application}
    +/secret/{default-context}/{profile}
    +/secret/{default-context}

    The application name is determined by the properties:

    • spring.cloud.vault.kv.application-name
    • spring.cloud.vault.application-name
    • spring.application.name

    Secrets can be obtained from other contexts within the key-value backend by adding their +paths to the application name, separated by commas. For example, given the application +name usefulapp,mysql1,projectx/aws, each of these folders will be used:

    • /secret/usefulapp
    • /secret/mysql1
    • /secret/projectx/aws

    Spring Cloud Vault adds all active profiles to the list of possible context paths. +No active profiles will skip accessing contexts with a profile name.

    Properties are exposed like they are stored (i.e. without additional prefixes).

    [Note]Note

    Spring Cloud Vault adds the data/ context between the mount path and the actual context path.

    spring.cloud.vault:
    +    kv:
    +        enabled: true
    +        backend: secret
    +        profile-separator: '/'
    +        default-context: application
    +        application-name: my-app
    • enabled setting this value to false disables the secret backend +config usage
    • backend sets the path of the secret mount to use
    • default-context sets the context name used by all applications
    • application-name overrides the application name for use in the generic backend
    • profile-separator separates the profile name from the context in +property sources with profiles
    [Note]Note

    The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable generic secret backend usage for non-versioned key-value backends and kv secret backend usage for versioned key-value backends.

    See also: Vault Documentation: Using the KV Secrets Engine - Version 2 (versioned key-value backend)

    103.3 Consul

    Spring Cloud Vault can obtain credentials for HashiCorp Consul. +The Consul integration requires the spring-cloud-vault-config-consul +dependency.

    Example 103.1. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-consul</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    The integration can be enabled by setting +spring.cloud.vault.consul.enabled=true (default false) and +providing the role name with spring.cloud.vault.consul.role=….

    The obtained token is stored in spring.cloud.consul.token +so using Spring Cloud Consul can pick up the generated +credentials without further configuration. You can configure +the property name by setting spring.cloud.vault.consul.token-property.

    spring.cloud.vault:
    +    consul:
    +        enabled: true
    +        role: readonly
    +        backend: consul
    +        token-property: spring.cloud.consul.token
    • enabled setting this value to true enables the Consul backend config usage
    • role sets the role name of the Consul role definition
    • backend sets the path of the Consul mount to use
    • token-property sets the property name in which the Consul ACL token is stored

    See also: Vault Documentation: Setting up Consul with Vault

    103.4 RabbitMQ

    Spring Cloud Vault can obtain credentials for RabbitMQ.

    The RabbitMQ integration requires the spring-cloud-vault-config-rabbitmq +dependency.

    Example 103.2. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-rabbitmq</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    The integration can be enabled by setting +spring.cloud.vault.rabbitmq.enabled=true (default false) +and providing the role name with spring.cloud.vault.rabbitmq.role=….

    Username and password are stored in spring.rabbitmq.username +and spring.rabbitmq.password so using Spring Boot will pick up the generated +credentials without further configuration. You can configure the property names +by setting spring.cloud.vault.rabbitmq.username-property and +spring.cloud.vault.rabbitmq.password-property.

    spring.cloud.vault:
    +    rabbitmq:
    +        enabled: true
    +        role: readonly
    +        backend: rabbitmq
    +        username-property: spring.rabbitmq.username
    +        password-property: spring.rabbitmq.password
    • enabled setting this value to true enables the RabbitMQ backend config usage
    • role sets the role name of the RabbitMQ role definition
    • backend sets the path of the RabbitMQ mount to use
    • username-property sets the property name in which the RabbitMQ username is stored
    • password-property sets the property name in which the RabbitMQ password is stored

    See also: Vault Documentation: Setting up RabbitMQ with Vault

    103.5 AWS

    Spring Cloud Vault can obtain credentials for AWS.

    The AWS integration requires the spring-cloud-vault-config-aws +dependency.

    Example 103.3. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-aws</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    The integration can be enabled by setting +spring.cloud.vault.aws=true (default false) +and providing the role name with spring.cloud.vault.aws.role=….

    The access key and secret key are stored in cloud.aws.credentials.accessKey +and cloud.aws.credentials.secretKey so using Spring Cloud AWS will pick up the generated +credentials without further configuration. You can configure the property names +by setting spring.cloud.vault.aws.access-key-property and +spring.cloud.vault.aws.secret-key-property.

    spring.cloud.vault:
    +    aws:
    +        enabled: true
    +        role: readonly
    +        backend: aws
    +        access-key-property: cloud.aws.credentials.accessKey
    +        secret-key-property: cloud.aws.credentials.secretKey
    • enabled setting this value to true enables the AWS backend config usage
    • role sets the role name of the AWS role definition
    • backend sets the path of the AWS mount to use
    • access-key-property sets the property name in which the AWS access key is stored
    • secret-key-property sets the property name in which the AWS secret key is stored

    See also: Vault Documentation: Setting up AWS with Vault

    104. Database backends

    Vault supports several database secret backends to generate database +credentials dynamically based on configured roles. This means +services that need to access a database no longer need to configure +credentials: they can request them from Vault, and use Vault’s leasing +mechanism to more easily roll keys.

    Spring Cloud Vault integrates with these backends:

    Using a database secret backend requires to enable the +backend in the configuration and the spring-cloud-vault-config-databases +dependency.

    Vault ships since 0.7.1 with a dedicated database secret backend that allows +database integration via plugins. You can use that specific backend by using the +generic database backend. Make sure to specify the appropriate +backend path, e.g. spring.cloud.vault.mysql.role.backend=database.

    Example 104.1. pom.xml

    <dependencies>
    +    <dependency>
    +        <groupId>org.springframework.cloud</groupId>
    +        <artifactId>spring-cloud-vault-config-databases</artifactId>
    +        <version>{project-version}</version>
    +    </dependency>
    +</dependencies>

    [Note]Note

    Enabling multiple JDBC-compliant databases will generate credentials +and store them by default in the same property keys hence property names for +JDBC secrets need to be configured separately.

    104.1 Database

    Spring Cloud Vault can obtain credentials for any database listed at +https://www.vaultproject.io/api/secret/databases/index.html. +The integration can be enabled by setting +spring.cloud.vault.database.enabled=true (default false) and +providing the role name with spring.cloud.vault.database.role=….

    While the database backend is a generic one, spring.cloud.vault.database +specifically targets JDBC databases. Username and password are +stored in spring.datasource.username and spring.datasource.password +so using Spring Boot will pick up the generated credentials +for your DataSource without further configuration. +You can configure the property names by setting +spring.cloud.vault.database.username-property and +spring.cloud.vault.database.password-property.

    spring.cloud.vault:
    +    database:
    +        enabled: true
    +        role: readonly
    +        backend: database
    +        username-property: spring.datasource.username
    +        password-property: spring.datasource.password
    • enabled setting this value to true enables the Database backend config usage
    • role sets the role name of the Database role definition
    • backend sets the path of the Database mount to use
    • username-property sets the property name in which the Database username is stored
    • password-property sets the property name in which the Database password is stored

    See also: Vault Documentation: Database Secrets backend

    [Warning]Warning

    Spring Cloud Vault does not support getting new credentials and +configuring your DataSource with them when the maximum lease time +has been reached. That is, if max_ttl of the Database role in Vault +is set to 24h that means that 24 hours after your application has +started it can no longer authenticate with the database.

    104.2 Apache Cassandra

    [Note]Note

    The cassandra backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as cassandra.

    Spring Cloud Vault can obtain credentials for Apache Cassandra. +The integration can be enabled by setting +spring.cloud.vault.cassandra.enabled=true (default false) and +providing the role name with spring.cloud.vault.cassandra.role=….

    Username and password are stored in spring.data.cassandra.username +and spring.data.cassandra.password so using Spring Boot will pick +up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.cassandra.username-property and +spring.cloud.vault.cassandra.password-property.

    spring.cloud.vault:
    +    cassandra:
    +        enabled: true
    +        role: readonly
    +        backend: cassandra
    +        username-property: spring.data.cassandra.username
    +        password-property: spring.data.cassandra.password
    • enabled setting this value to true enables the Cassandra backend config usage
    • role sets the role name of the Cassandra role definition
    • backend sets the path of the Cassandra mount to use
    • username-property sets the property name in which the Cassandra username is stored
    • password-property sets the property name in which the Cassandra password is stored

    See also: Vault Documentation: Setting up Apache Cassandra with Vault

    104.3 MongoDB

    [Note]Note

    The mongodb backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as mongodb.

    Spring Cloud Vault can obtain credentials for MongoDB. +The integration can be enabled by setting +spring.cloud.vault.mongodb.enabled=true (default false) and +providing the role name with spring.cloud.vault.mongodb.role=….

    Username and password are stored in spring.data.mongodb.username +and spring.data.mongodb.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.mongodb.username-property and +spring.cloud.vault.mongodb.password-property.

    spring.cloud.vault:
    +    mongodb:
    +        enabled: true
    +        role: readonly
    +        backend: mongodb
    +        username-property: spring.data.mongodb.username
    +        password-property: spring.data.mongodb.password
    • enabled setting this value to true enables the MongodB backend config usage
    • role sets the role name of the MongoDB role definition
    • backend sets the path of the MongoDB mount to use
    • username-property sets the property name in which the MongoDB username is stored
    • password-property sets the property name in which the MongoDB password is stored

    See also: Vault Documentation: Setting up MongoDB with Vault

    104.4 MySQL

    [Note]Note

    The mysql backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as mysql. +Configuration for spring.cloud.vault.mysql will be removed in a future version.

    Spring Cloud Vault can obtain credentials for MySQL. +The integration can be enabled by setting +spring.cloud.vault.mysql.enabled=true (default false) and +providing the role name with spring.cloud.vault.mysql.role=….

    Username and password are stored in spring.datasource.username +and spring.datasource.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.mysql.username-property and +spring.cloud.vault.mysql.password-property.

    spring.cloud.vault:
    +    mysql:
    +        enabled: true
    +        role: readonly
    +        backend: mysql
    +        username-property: spring.datasource.username
    +        password-property: spring.datasource.password
    • enabled setting this value to true enables the MySQL backend config usage
    • role sets the role name of the MySQL role definition
    • backend sets the path of the MySQL mount to use
    • username-property sets the property name in which the MySQL username is stored
    • password-property sets the property name in which the MySQL password is stored

    See also: Vault Documentation: Setting up MySQL with Vault

    104.5 PostgreSQL

    [Note]Note

    The postgresql backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as postgresql. +Configuration for spring.cloud.vault.postgresql will be removed in a future version.

    Spring Cloud Vault can obtain credentials for PostgreSQL. +The integration can be enabled by setting +spring.cloud.vault.postgresql.enabled=true (default false) and +providing the role name with spring.cloud.vault.postgresql.role=….

    Username and password are stored in spring.datasource.username +and spring.datasource.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.postgresql.username-property and +spring.cloud.vault.postgresql.password-property.

    spring.cloud.vault:
    +    postgresql:
    +        enabled: true
    +        role: readonly
    +        backend: postgresql
    +        username-property: spring.datasource.username
    +        password-property: spring.datasource.password
    • enabled setting this value to true enables the PostgreSQL backend config usage
    • role sets the role name of the PostgreSQL role definition
    • backend sets the path of the PostgreSQL mount to use
    • username-property sets the property name in which the PostgreSQL username is stored
    • password-property sets the property name in which the PostgreSQL password is stored

    See also: Vault Documentation: Setting up PostgreSQL with Vault

    105. Configure PropertySourceLocator behavior

    Spring Cloud Vault uses property-based configuration to create PropertySources +for generic and discovered secret backends.

    Discovered backends provide VaultSecretBackendDescriptor beans to describe the configuration +state to use secret backend as PropertySource. A SecretBackendMetadataFactory is required +to create a SecretBackendMetadata object which contains path, name and property transformation +configuration.

    SecretBackendMetadata is used to back a particular PropertySource.

    You can register an arbitrary number of beans implementing VaultConfigurer for customization. +Default generic and discovered backend registration is disabled if Spring Cloud Vault discovers +at least one VaultConfigurer bean. You can however enable default registration with +SecretBackendConfigurer.registerDefaultGenericSecretBackends() and SecretBackendConfigurer.registerDefaultDiscoveredSecretBackends().

    public class CustomizationBean implements VaultConfigurer {
    +
    +    @Override
    +    public void addSecretBackends(SecretBackendConfigurer configurer) {
    +
    +        configurer.add("secret/my-application");
    +
    +        configurer.registerDefaultGenericSecretBackends(false);
    +        configurer.registerDefaultDiscoveredSecretBackends(true);
    +    }
    +}
    [Note]Note

    All customization is required to happen in the bootstrap context. Add your configuration +classes to META-INF/spring.factories at org.springframework.cloud.bootstrap.BootstrapConfiguration +in your application.

    106. Service Registry Configuration

    You can use a DiscoveryClient (such as from Spring Cloud Consul) to locate +a Vault server by setting spring.cloud.vault.discovery.enabled=true (default false). +The net result of that is that your apps need a bootstrap.yml (or an environment variable) +with the appropriate discovery configuration. +The benefit is that the Vault can change its co-ordinates, as long as the discovery service +is a fixed point. The default service id is vault but you can change that on the client with +spring.cloud.vault.discovery.serviceId.

    The discovery client implementations all support some kind of metadata map +(e.g. for Eureka we have eureka.instance.metadataMap). Some additional properties of the service +may need to be configured in its service registration metadata so that clients can connect +correctly. Service registries that do not provide details about transport layer security +need to provide a scheme metadata entry to be set either to https or http. +If no scheme is configured and the service is not exposed as secure service, then +configuration defaults to spring.cloud.vault.scheme which is https when it’s not set.

    spring.cloud.vault.discovery:
    +    enabled: true
    +    service-id: my-vault-service

    107. Vault Client Fail Fast

    In some cases, it may be desirable to fail startup of a service if +it cannot connect to the Vault Server. If this is the desired +behavior, set the bootstrap configuration property +spring.cloud.vault.fail-fast=true and the client will halt with +an Exception.

    spring.cloud.vault:
    +    fail-fast: true

    108. Vault Client SSL configuration

    SSL can be configured declaratively by setting various properties. +You can set either javax.net.ssl.trustStore to configure +JVM-wide SSL settings or spring.cloud.vault.ssl.trust-store +to set SSL settings only for Spring Cloud Vault Config.

    spring.cloud.vault:
    +    ssl:
    +        trust-store: classpath:keystore.jks
    +        trust-store-password: changeit
    • trust-store sets the resource for the trust-store. SSL-secured Vault +communication will validate the Vault SSL certificate with the specified +trust-store.
    • trust-store-password sets the trust-store password

    Please note that configuring spring.cloud.vault.ssl.* can be only +applied when either Apache Http Components or the OkHttp client +is on your class-path.

    109. Lease lifecycle management (renewal and revocation)

    With every secret, Vault creates a lease: +metadata containing information such as a time duration, +renewability, and more.

    Vault promises that the data will be valid for the given duration, +or Time To Live (TTL). Once the lease is expired, Vault can +revoke the data, and the consumer of the secret can no longer +be certain that it is valid.

    Spring Cloud Vault maintains a lease lifecycle beyond +the creation of login tokens and secrets. That said, +login tokens and secrets associated with a lease +are scheduled for renewal just before the lease expires +until terminal expiry. +Application shutdown revokes obtained login tokens and renewable +leases.

    Secret service and database backends (such as MongoDB or MySQL) +usually generate a renewable lease so generated credentials will +be disabled on application shutdown.

    [Note]Note

    Static tokens are not renewed or revoked.

    Lease renewal and revocation is enabled by default and can +be disabled by setting spring.cloud.vault.config.lifecycle.enabled +to false. This is not recommended as leases can expire and +Spring Cloud Vault cannot longer access Vault or services +using generated credentials and valid credentials remain active +after application shutdown.

    spring.cloud.vault:
    +    config.lifecycle.enabled: true

    See also: Vault Documentation: Lease, Renew, and Revoke

    Part XV. Spring Cloud Gateway

    Greenwich.SR5

    This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

    110. How to Include Spring Cloud Gateway

    To include Spring Cloud Gateway in your project use the starter with group org.springframework.cloud +and artifact id spring-cloud-starter-gateway. See the Spring Cloud Project page +for details on setting up your build system with the current Spring Cloud Release Train.

    If you include the starter, but, for some reason, you do not want the gateway to be enabled, set spring.cloud.gateway.enabled=false.

    [Important]Important

    Spring Cloud Gateway is built upon Spring Boot 2.x, +Spring WebFlux, +and Project Reactor. As a consequence +many of the familiar synchronous libraries (Spring Data and Spring Security, for example) and patterns you may +not apply when using Spring Cloud Gateway. If you are unfamiliar with these projects we suggest you +begin by reading their documentation to familiarize yourself with some of the new concepts before +working with Spring Cloud Gateway.

    [Important]Important

    Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It does not work in a traditional Servlet Container or built as a WAR.

    111. Glossary

    • Route: Route the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true.
    • Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This allows developers to match on anything from the HTTP request, such as headers or parameters.
    • Filter: These are instances Spring Framework GatewayFilter constructed in with a specific factory. Here, requests and responses can be modified before or after sending the downstream request.

    112. How It Works

    Spring Cloud Gateway Diagram

    Clients make requests to Spring Cloud Gateway. If the Gateway Handler Mapping determines that a request matches a Route, it is sent to the Gateway Web Handler. This handler runs sends the request through a filter chain that is specific to the request. The reason the filters are divided by the dotted line, is that filters may execute logic before the proxy request is sent or after. All "pre" filter logic is executed, then the proxy request is made. After the proxy request is made, the "post" filter logic is executed.

    [Note]Note

    URIs defined in routes without a port will get a default port set to 80 and 443 for HTTP and HTTPS URIs respectively.

    113. Configuring Route Predicate Factories and Gateway Filter Factories

    There are two ways to configure predicates and filters: shortcuts and fully expanded arguments. Most examples below use the shortcut way.

    The name and argument names will be listed as code in the first sentance or two of the each section. The arguments are typically listed in the order that would be needed for the shortcut configuration.

    113.1 Shortcut Configuration

    Shortcut configuration is recognized by the filter name, followed by an equals sign (=), followed by argument values separated by commas (,).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: after_route
    +        uri: https://example.org
    +        predicates:
    +        - Cookie=mycookie,mycookievalue

    +

    The previous sample defines the Cookie Route Predicate Factory with two arguments, the cookie name, mycookie and the value to match mycookievalue.

    113.2 Fully Expanded Arguments

    Fully expanded arguments appear more like standard yaml configuration with name/value pairs. Typically, there will be a name key and an args key. The args key is a map of key value pairs to configure the predicate or filter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: after_route
    +        uri: https://example.org
    +        predicates:
    +        - name: Cookie
    +          args:
    +            name: mycookie
    +            regexp: mycookievalue

    +

    This is the full configuration of the shortcut configuration of the Cookie predicate shown above.

    114. Route Predicate Factories

    Spring Cloud Gateway matches routes as part of the Spring WebFlux HandlerMapping infrastructure. Spring Cloud Gateway includes many built-in Route Predicate Factories. All of these predicates match on different attributes of the HTTP request. Multiple Route Predicate Factories can be combined and are combined via logical and.

    114.1 After Route Predicate Factory

    The After Route Predicate Factory takes one parameter, a datetime (which is a java ZonedDateTime). This predicate matches requests that happen after the current datetime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: after_route
    +        uri: https://example.org
    +        predicates:
    +        - After=2017-01-20T17:42:47.789-07:00[America/Denver]

    +

    This route matches any request after Jan 20, 2017 17:42 Mountain Time (Denver).

    114.2 Before Route Predicate Factory

    The Before Route Predicate Factory takes one parameter, a datetime(which is a java ZonedDateTime). This predicate matches requests that happen before the current datetime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: before_route
    +        uri: https://example.org
    +        predicates:
    +        - Before=2017-01-20T17:42:47.789-07:00[America/Denver]

    +

    This route matches any request before Jan 20, 2017 17:42 Mountain Time (Denver).

    114.3 Between Route Predicate Factory

    The Between Route Predicate Factory takes two parameters, datetime1 and datetime2 which are java ZonedDateTime objects. This predicate matches requests that happen after datetime1 and before datetime2. The datetime2 parameter must be after datetime1.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: between_route
    +        uri: https://example.org
    +        predicates:
    +        - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

    +

    This route matches any request after Jan 20, 2017 17:42 Mountain Time (Denver) and before Jan 21, 2017 17:42 Mountain Time (Denver). This could be useful for maintenance windows.

    114.4 Cookie Route Predicate Factory

    The Cookie Route Predicate Factory takes two parameters, the cookie name and a regexp (which is a Java regular expression). This predicate matches cookies that have the given name and the value matches the regular expression.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: cookie_route
    +        uri: https://example.org
    +        predicates:
    +        - Cookie=chocolate, ch.p

    +

    This route matches the request has a cookie named chocolate who’s value matches the ch.p regular expression.

    114.5 Header Route Predicate Factory

    The Header Route Predicate Factory takes two parameters, the header name and a regexp (which is a Java regular expression). This predicate matches with a header that has the given name and the value matches the regular expression.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: header_route
    +        uri: https://example.org
    +        predicates:
    +        - Header=X-Request-Id, \d+

    +

    This route matches if the request has a header named X-Request-Id whos value matches the \d+ regular expression (has a value of one or more digits).

    114.6 Host Route Predicate Factory

    The Host Route Predicate Factory takes one parameter: a list of host name patterns. The pattern is an Ant style pattern with . as the separator. This predicates matches the Host header that matches the pattern.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: host_route
    +        uri: https://example.org
    +        predicates:
    +        - Host=**.somehost.org,**.anotherhost.org

    +

    URI template variables are supported as well, such as {sub}.myhost.org.

    This route would match if the request has a Host header has the value www.somehost.org or beta.somehost.org or www.anotherhost.org.

    This predicate extracts the URI template variables (like sub defined in the example above) as a map of names and values and places it in the ServerWebExchange.getAttributes() with a key defined in ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE. Those values are then available for use by GatewayFilter Factories

    114.7 Method Route Predicate Factory

    The Method Route Predicate Factory takes a methods argument which is one or more HTTP methods to match.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: method_route
    +        uri: https://example.org
    +        predicates:
    +        - Method=GET,POST

    +

    This route would match if the request method was a GET or a POST.

    114.8 Path Route Predicate Factory

    The Path Route Predicate Factory takes two parameter: a list of Spring PathMatcher patterns and an optional flag to matchOptionalTrailingSeparator.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: host_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/{segment},/bar/{segment}

    +

    This route would match if the request path was, for example: /foo/1 or /foo/bar or /bar/baz.

    This predicate extracts the URI template variables (like segment defined in the example above) as a map of names and values and places it in the ServerWebExchange.getAttributes() with a key defined in ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE. Those values are then available for use by GatewayFilter Factories

    A utility method is available to make access to these variables easier.

    Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);
    +
    +String segment = uriVariables.get("segment");

    114.9 Query Route Predicate Factory

    The Query Route Predicate Factory takes two parameters: a required param and an optional regexp (which is a Java regular expression).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: query_route
    +        uri: https://example.org
    +        predicates:
    +        - Query=baz

    +

    This route would match if the request contained a baz query parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: query_route
    +        uri: https://example.org
    +        predicates:
    +        - Query=foo, ba.

    +

    This route would match if the request contained a foo query parameter whose value matched the ba. regexp, so bar and baz would match.

    114.10 RemoteAddr Route Predicate Factory

    The RemoteAddr Route Predicate Factory takes a list (min size 1) of sources, which are CIDR-notation (IPv4 or IPv6) strings, e.g. 192.168.0.1/16 (where 192.168.0.1 is an IP address and 16 is a subnet mask).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: remoteaddr_route
    +        uri: https://example.org
    +        predicates:
    +        - RemoteAddr=192.168.1.1/24

    +

    This route would match if the remote address of the request was, for example, 192.168.1.10.

    114.11 Weight Route Predicate Factory

    The Weight Route Predicate Factory takes two arguments group and weight (an int). The weights are calculated per group.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: weight_high
    +        uri: https://weighthigh.org
    +        predicates:
    +        - Weight=group1, 8
    +      - id: weight_low
    +        uri: https://weightlow.org
    +        predicates:
    +        - Weight=group1, 2

    +

    This route would forward ~80% of traffic to https://weighthigh.org and ~20% of traffic to https://weighlow.org

    114.11.1 Modifying the way remote addresses are resolved

    By default the RemoteAddr Route Predicate Factory uses the remote address from the incoming request. +This may not match the actual client IP address if Spring Cloud Gateway sits behind a proxy layer.

    You can customize the way that the remote address is resolved by setting a custom RemoteAddressResolver. +Spring Cloud Gateway comes with one non-default remote address resolver which is based off of the X-Forwarded-For header, XForwardedRemoteAddressResolver.

    XForwardedRemoteAddressResolver has two static constructor methods which take different approaches to security:

    XForwardedRemoteAddressResolver::trustAll returns a RemoteAddressResolver which always takes the first IP address found in the X-Forwarded-For header. +This approach is vulnerable to spoofing, as a malicious client could set an initial value for the X-Forwarded-For which would be accepted by the resolver.

    XForwardedRemoteAddressResolver::maxTrustedIndex takes an index which correlates to the number of trusted infrastructure running in front of Spring Cloud Gateway. +If Spring Cloud Gateway is, for example only accessible via HAProxy, then a value of 1 should be used. +If two hops of trusted infrastructure are required before Spring Cloud Gateway is accessible, then a value of 2 should be used.

    Given the following header value:

    X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3

    The maxTrustedIndex values below will yield the following remote addresses.

    maxTrustedIndexresult

    [Integer.MIN_VALUE,0]

    (invalid, IllegalArgumentException during initialization)

    1

    0.0.0.3

    2

    0.0.0.2

    3

    0.0.0.1

    [4, Integer.MAX_VALUE]

    0.0.0.1

    Using Java config:

    GatewayConfig.java

    RemoteAddressResolver resolver = XForwardedRemoteAddressResolver
    +    .maxTrustedIndex(1);
    +
    +...
    +
    +.route("direct-route",
    +    r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24")
    +        .uri("https://downstream1")
    +.route("proxied-route",
    +    r -> r.remoteAddr(resolver,  "10.10.1.1", "10.10.1.1/24")
    +        .uri("https://downstream2")
    +)

    115. GatewayFilter Factories

    Route filters allow the modification of the incoming HTTP request or outgoing HTTP response in some manner. Route filters are scoped to a particular route. Spring Cloud Gateway includes many built-in GatewayFilter Factories.

    NOTE For more detailed examples on how to use any of the following filters, take a look at the unit tests.

    115.1 AddRequestHeader GatewayFilter Factory

    The AddRequestHeader GatewayFilter Factory takes a name and value parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_header_route
    +        uri: https://example.org
    +        filters:
    +        - AddRequestHeader=X-Request-Foo, Bar

    +

    This will add X-Request-Foo:Bar header to the downstream request’s headers for all matching requests.

    AddRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_header_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/{segment}
    +        filters:
    +        - AddRequestHeader=X-Request-Foo, Bar-{segment}

    +

    115.2 AddRequestParameter GatewayFilter Factory

    The AddRequestParameter GatewayFilter Factory takes a name and value parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_parameter_route
    +        uri: https://example.org
    +        filters:
    +        - AddRequestParameter=foo, bar

    +

    This will add foo=bar to the downstream request’s query string for all matching requests.

    AddRequestParameter is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_request_parameter_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - AddRequestParameter=foo, bar-{segment}

    +

    115.3 AddResponseHeader GatewayFilter Factory

    The AddResponseHeader GatewayFilter Factory takes a name and value parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_response_header_route
    +        uri: https://example.org
    +        filters:
    +        - AddResponseHeader=X-Response-Foo, Bar

    +

    This will add X-Response-Foo:Bar header to the downstream response’s headers for all matching requests.

    AddResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: add_response_header_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - AddResponseHeader=foo, bar-{segment}

    +

    115.4 DedupeResponseHeader GatewayFilter Factory

    The DedupeResponseHeader GatewayFilter Factory takes a name parameter and an optional strategy parameter. name can contain a list of header names, space separated.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: dedupe_response_header_route
    +        uri: https://example.org
    +        filters:
    +        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

    +

    This will remove duplicate values of Access-Control-Allow-Credentials and Access-Control-Allow-Origin response headers in cases when both the gateway CORS logic and the downstream add them.

    The DedupeResponseHeader filter also accepts an optional strategy parameter. The accepted values are RETAIN_FIRST (default), RETAIN_LAST, and RETAIN_UNIQUE.

    115.5 Hystrix GatewayFilter Factory

    Hystrix is a library from Netflix that implements the circuit breaker pattern. +The Hystrix GatewayFilter allows you to introduce circuit breakers to your gateway routes, protecting your services from cascading failures and allowing you to provide fallback responses in the event of downstream failures.

    To enable Hystrix GatewayFilters in your project, add a dependency on spring-cloud-starter-netflix-hystrix from Spring Cloud Netflix.

    The Hystrix GatewayFilter Factory requires a single name parameter, which is the name of the HystrixCommand.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: hystrix_route
    +        uri: https://example.org
    +        filters:
    +        - Hystrix=myCommandName

    +

    This wraps the remaining filters in a HystrixCommand with command name myCommandName.

    The Hystrix filter can also accept an optional fallbackUri parameter. Currently, only forward: schemed URIs are supported. If the fallback is called, the request will be forwarded to the controller matched by the URI.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: hystrix_route
    +        uri: lb://backing-service:8088
    +        predicates:
    +        - Path=/consumingserviceendpoint
    +        filters:
    +        - name: Hystrix
    +          args:
    +            name: fallbackcmd
    +            fallbackUri: forward:/incaseoffailureusethis
    +        - RewritePath=/consumingserviceendpoint, /backingserviceendpoint

    +

    This will forward to the /incaseoffailureusethis URI when the Hystrix fallback is called. Note that this example also demonstrates (optional) Spring Cloud Netflix Ribbon load-balancing via the lb prefix on the destination URI.

    The primary scenario is to use the fallbackUri to an internal controller or handler within the gateway app. +However, it is also possible to reroute the request to a controller or handler in an external application, like so:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: ingredients
    +        uri: lb://ingredients
    +        predicates:
    +        - Path=//ingredients/**
    +        filters:
    +        - name: Hystrix
    +          args:
    +            name: fetchIngredients
    +            fallbackUri: forward:/fallback
    +      - id: ingredients-fallback
    +        uri: http://localhost:9994
    +        predicates:
    +        - Path=/fallback

    +

    In this example, there is no fallback endpoint or handler in the gateway application, however, there is one in another +app, registered under http://localhost:9994.

    In case of the request being forwarded to fallback, the Hystrix Gateway filter also provides the Throwable that has +caused it. It’s added to the ServerWebExchange as the +ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR attribute that can be used when +handling the fallback within the gateway app.

    For the external controller/ handler scenario, headers can be added with exception details. You can find more information +on it in the FallbackHeaders GatewayFilter Factory section.

    Hystrix settings (such as timeouts) can be configured with global defaults or on a route by route basis using application properties as explained on the Hystrix wiki.

    To set a 5 second timeout for the example route above, the following configuration would be used:

    application.yml.  +

    hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000

    +

    115.6 FallbackHeaders GatewayFilter Factory

    The FallbackHeaders factory allows you to add Hystrix execution exception details in headers of a request forwarded to +a fallbackUri in an external application, like in the following scenario:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: ingredients
    +        uri: lb://ingredients
    +        predicates:
    +        - Path=//ingredients/**
    +        filters:
    +        - name: Hystrix
    +          args:
    +            name: fetchIngredients
    +            fallbackUri: forward:/fallback
    +      - id: ingredients-fallback
    +        uri: http://localhost:9994
    +        predicates:
    +        - Path=/fallback
    +        filters:
    +        - name: FallbackHeaders
    +          args:
    +            executionExceptionTypeHeaderName: Test-Header

    +

    In this example, after an execution exception occurs while running the HystrixCommand, the request will be forwarde to +the fallback endpoint or handler in an app running on localhost:9994. The headers with the exception type, message +and -if available- root cause exception type and message will be added to that request by the FallbackHeaders filter.

    The names of the headers can be overwritten in the config by setting the values of the arguments listed below, along with +their default values:

    • executionExceptionTypeHeaderName ("Execution-Exception-Type")
    • executionExceptionMessageHeaderName ("Execution-Exception-Message")
    • rootCauseExceptionTypeHeaderName ("Root-Cause-Exception-Type")
    • rootCauseExceptionMessageHeaderName ("Root-Cause-Exception-Message")

    You can find more information on how Hystrix works with Gateway in the Hystrix GatewayFilter Factory section.

    115.7 MapRequestHeader GatewayFilter Factory

    The MapRequestHeader GatewayFilter Facstory takes 'fromHeader' and 'toHeader' parameters. It creates a new named header (toHeader) and the value is extracted out of an existing named header (fromHeader) from the incoming http request. If the input header does not exist then the filter has no impact. If the new named header already exists then it’s values will be augmented with the new values.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: map_request_header_route
    +        uri: https://example.org
    +        filters:
    +        - MapRequestHeader=Bar, X-Request-Foo

    +

    This will add X-Request-Foo:<values> header to the downstream request’s with updated values from the incoming http request Bar header.

    115.8 PrefixPath GatewayFilter Factory

    The PrefixPath GatewayFilter Factory takes a single prefix parameter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: prefixpath_route
    +        uri: https://example.org
    +        filters:
    +        - PrefixPath=/mypath

    +

    This will prefix /mypath to the path of all matching requests. So a request to /hello, would be sent to /mypath/hello.

    115.9 PreserveHostHeader GatewayFilter Factory

    The PreserveHostHeader GatewayFilter Factory has no parameters. This filter, sets a request attribute that the routing filter will inspect to determine if the original host header should be sent, rather than the host header determined by the http client.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: preserve_host_route
    +        uri: https://example.org
    +        filters:
    +        - PreserveHostHeader

    +

    115.10 RequestRateLimiter GatewayFilter Factory

    The RequestRateLimiter GatewayFilter Factory is uses a RateLimiter implementation to determine if the current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by default) is returned.

    This filter takes an optional keyResolver parameter and parameters specific to the rate limiter (see below).

    keyResolver is a bean that implements the KeyResolver interface. In configuration, reference the bean by name using SpEL. #{@myKeyResolver} is a SpEL expression referencing a bean with the name myKeyResolver.

    KeyResolver.java.  +

    public interface KeyResolver {
    +	Mono<String> resolve(ServerWebExchange exchange);
    +}

    +

    The KeyResolver interface allows pluggable strategies to derive the key for limiting requests. In future milestones, there will be some KeyResolver implementations.

    The default implementation of KeyResolver is the PrincipalNameKeyResolver which retrieves the Principal from the ServerWebExchange and calls Principal.getName().

    By default, if the KeyResolver does not find a key, requests will be denied. This behavior can be adjust with the spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key (true or false) and spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code properties.

    [Note]Note

    The RequestRateLimiter is not configurable via the "shortcut" notation. The example below is invalid

    application.properties.  +

    # INVALID SHORTCUT CONFIGURATION
    +spring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2, 2, #{@userkeyresolver}

    +

    115.10.1 Redis RateLimiter

    The redis implementation is based off of work done at Stripe. It requires the use of the spring-boot-starter-data-redis-reactive Spring Boot starter.

    The algorithm used is the Token Bucket Algorithm.

    The redis-rate-limiter.replenishRate is how many requests per second do you want a user to be allowed to do, without any dropped requests. This is the rate that the token bucket is filled.

    The redis-rate-limiter.burstCapacity is the maximum number of requests a user is allowed to do in a single second. This is the number of tokens the token bucket can hold. Setting this value to zero will block all requests.

    A steady rate is accomplished by setting the same value in replenishRate and burstCapacity. Temporary bursts can be allowed by setting burstCapacity higher than replenishRate. In this case, the rate limiter needs to be allowed some time between bursts (according to replenishRate), as 2 consecutive bursts will result in dropped requests (HTTP 429 - Too Many Requests).

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: requestratelimiter_route
    +        uri: https://example.org
    +        filters:
    +        - name: RequestRateLimiter
    +          args:
    +            redis-rate-limiter.replenishRate: 10
    +            redis-rate-limiter.burstCapacity: 20

    +

    Config.java.  +

    @Bean
    +KeyResolver userKeyResolver() {
    +    return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    +}

    +

    This defines a request rate limit of 10 per user. A burst of 20 is allowed, but the next second only 10 requests will be available. The KeyResolver is a simple one that gets the user request parameter (note: this is not recommended for production).

    A rate limiter can also be defined as a bean implementing the RateLimiter interface. In configuration, reference the bean by name using SpEL. #{@myRateLimiter} is a SpEL expression referencing a bean with the name myRateLimiter.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: requestratelimiter_route
    +        uri: https://example.org
    +        filters:
    +        - name: RequestRateLimiter
    +          args:
    +            rate-limiter: "#{@myRateLimiter}"
    +            key-resolver: "#{@userKeyResolver}"

    +

    115.11 RedirectTo GatewayFilter Factory

    The RedirectTo GatewayFilter Factory takes a status and a url parameter. The status should be a 300 series redirect http code, such as 301. The url should be a valid url. This will be the value of the Location header.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: prefixpath_route
    +        uri: https://example.org
    +        filters:
    +        - RedirectTo=302, https://acme.org

    +

    This will send a status 302 with a Location:https://acme.org header to perform a redirect.

    115.12 RemoveRequestHeader GatewayFilter Factory

    The RemoveRequestHeader GatewayFilter Factory takes a name parameter. It is the name of the header to be removed.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: removerequestheader_route
    +        uri: https://example.org
    +        filters:
    +        - RemoveRequestHeader=X-Request-Foo

    +

    This will remove the X-Request-Foo header before it is sent downstream.

    115.13 RemoveResponseHeader GatewayFilter Factory

    The RemoveResponseHeader GatewayFilter Factory takes a name parameter. It is the name of the header to be removed.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: removeresponseheader_route
    +        uri: https://example.org
    +        filters:
    +        - RemoveResponseHeader=X-Response-Foo

    +

    This will remove the X-Response-Foo header from the response before it is returned to the gateway client.

    To remove any kind of sensitive header you should configure this filter for any routes that you may +want to do so. In addition you can configure this filter once using spring.cloud.gateway.default-filters +and have it applied to all routes.

    115.14 RewritePath GatewayFilter Factory

    The RewritePath GatewayFilter Factory takes a path regexp parameter and a replacement parameter. This uses Java regular expressions for a flexible way to rewrite the request path.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: rewritepath_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/**
    +        filters:
    +        - RewritePath=/foo(?<segment>/?.*), $\{segment}

    +

    For a request path of /foo/bar, this will set the path to /bar before making the downstream request. Notice the $\ which is replaced with $ because of the YAML spec.

    115.15 RewriteLocationResponseHeader GatewayFilter Factory

    The RewriteLocationResponseHeader GatewayFilter Factory modifies the value of Location response header, usually to get rid of backend specific details. It takes stripVersionMode, locationHeaderName, hostValue, and protocolsRegex parameters.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: rewritelocationresponseheader_route
    +        uri: http://example.org
    +        filters:
    +        - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,

    +

    For example, for a request POST https://api.example.com/some/object/name, Location response header value https://object-service.prod.example.net/v2/some/object/id will be rewritten as https://api.example.com/some/object/id.

    Parameter stripVersionMode has the following possible values: NEVER_STRIP, AS_IN_REQUEST (default), ALWAYS_STRIP.

    • NEVER_STRIP - Version will not be stripped, even if the original request path contains no version
    • AS_IN_REQUEST - Version will be stripped only if the original request path contains no version
    • ALWAYS_STRIP - Version will be stripped, even if the original request path contains version

    Parameter hostValue, if provided, will be used to replace the host:port portion of the response Location header. If not provided, the value of the Host request header will be used.

    Parameter protocolsRegex must be a valid regex String, against which the protocol name will be matched. If not matched, the filter will do nothing. Default is http|https|ftp|ftps.

    115.16 RewriteResponseHeader GatewayFilter Factory

    The RewriteResponseHeader GatewayFilter Factory takes name, regexp, and replacement parameters. It uses Java regular expressions for a flexible way to rewrite the response header value.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: rewriteresponseheader_route
    +        uri: https://example.org
    +        filters:
    +        - RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=***

    +

    For a header value of /42?user=ford&password=omg!what&flag=true, it will be set to /42?user=ford&password=***&flag=true after making the downstream request. Please use $\ to mean $ because of the YAML spec.

    115.17 SaveSession GatewayFilter Factory

    The SaveSession GatewayFilter Factory forces a WebSession::save operation before forwarding the call downstream. This is of particular use when +using something like Spring Session with a lazy data store and need to ensure the session state has been saved before making the forwarded call.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: save_session
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/**
    +        filters:
    +        - SaveSession

    +

    If you are integrating Spring Security with Spring Session, and want to ensure security details have been forwarded to the remote process, this is critical.

    115.18 SecureHeaders GatewayFilter Factory

    The SecureHeaders GatewayFilter Factory adds a number of headers to the response at the recommendation from this blog post.

    The following headers are added (along with default values):

    • X-Xss-Protection:1; mode=block
    • Strict-Transport-Security:max-age=631138519
    • X-Frame-Options:DENY
    • X-Content-Type-Options:nosniff
    • Referrer-Policy:no-referrer
    • Content-Security-Policy:default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'
    • X-Download-Options:noopen
    • X-Permitted-Cross-Domain-Policies:none

    To change the default values set the appropriate property in the spring.cloud.gateway.filter.secure-headers namespace:

    Property to change:

    • xss-protection-header
    • strict-transport-security
    • frame-options
    • content-type-options
    • referrer-policy
    • content-security-policy
    • download-options
    • permitted-cross-domain-policies

    To disable the default values set the property spring.cloud.gateway.filter.secure-headers.disable with comma separated values.

    [Note]Note

    Need use lowercase and full name of secure headers.

    The following values can use:

    • x-xss-protection
    • strict-transport-security
    • x-frame-options
    • x-content-type-options
    • referrer-policy
    • content-security-policy
    • x-download-options
    • x-permitted-cross-domain-policies

    Example: spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security

    115.19 SetPath GatewayFilter Factory

    The SetPath GatewayFilter Factory takes a path template parameter. It offers a simple way to manipulate the request path by allowing templated segments of the path. This uses the uri templates from Spring Framework. Multiple matching segments are allowed.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setpath_route
    +        uri: https://example.org
    +        predicates:
    +        - Path=/foo/{segment}
    +        filters:
    +        - SetPath=/{segment}

    +

    For a request path of /foo/bar, this will set the path to /bar before making the downstream request.

    115.20 SetRequestHeader GatewayFilter Factory

    The SetRequestHeader GatewayFilter Factory takes name and value parameters.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setrequestheader_route
    +        uri: https://example.org
    +        filters:
    +        - SetRequestHeader=X-Request-Foo, Bar

    +

    This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a X-Request-Foo:1234, this would be replaced with X-Request-Foo:Bar, which is what the downstream service would receive.

    SetRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setrequestheader_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - SetRequestHeader=foo, bar-{segment}

    +

    115.21 SetResponseHeader GatewayFilter Factory

    The SetResponseHeader GatewayFilter Factory takes name and value parameters.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setresponseheader_route
    +        uri: https://example.org
    +        filters:
    +        - SetResponseHeader=X-Response-Foo, Bar

    +

    This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a X-Response-Foo:1234, this would be replaced with X-Response-Foo:Bar, which is what the gateway client would receive.

    SetResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setresponseheader_route
    +        uri: https://example.org
    +        predicates:
    +        - Host: {segment}.myhost.org
    +        filters:
    +        - SetResponseHeader=foo, bar-{segment}

    +

    115.22 SetStatus GatewayFilter Factory

    The SetStatus GatewayFilter Factory takes a single status parameter. It must be a valid Spring HttpStatus. It may be the integer value 404 or the string representation of the enumeration NOT_FOUND.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setstatusstring_route
    +        uri: https://example.org
    +        filters:
    +        - SetStatus=BAD_REQUEST
    +      - id: setstatusint_route
    +        uri: https://example.org
    +        filters:
    +        - SetStatus=401

    +

    In either case, the HTTP status of the response will be set to 401.

    115.23 StripPrefix GatewayFilter Factory

    The StripPrefix GatewayFilter Factory takes one paramter, parts. The parts parameter indicated the number of parts in the path to strip from the request before sending it downstream.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: nameRoot
    +        uri: http://nameservice
    +        predicates:
    +        - Path=/name/**
    +        filters:
    +        - StripPrefix=2

    +

    When a request is made through the gateway to /name/bar/foo the request made to nameservice will look like http://nameservice/foo.

    115.24 Retry GatewayFilter Factory

    The Retry GatewayFilter Factory support following set of parameters:

    • retries: the number of retries that should be attempted
    • statuses: the HTTP status codes that should be retried, represented using org.springframework.http.HttpStatus
    • methods: the HTTP methods that should be retried, represented using org.springframework.http.HttpMethod
    • series: the series of status codes to be retried, represented using org.springframework.http.HttpStatus.Series
    • exceptions: list of exceptions thrown that should be retried
    • backoff: configured exponential backoff for the retries. Retries are performed after a backoff interval of firstBackoff * (factor ^ n) where n is the iteration. +If maxBackoff is configured, the maximum backoff applied will be limited to maxBackoff. +If basedOnPreviousValue is true, backoff will be calculated using prevBackoff * factor.

    The following defaults are configured for Retry filter if enabled:

    • retries — 3 times
    • series — 5XX series
    • methods — GET method
    • exceptions — IOException and TimeoutException
    • backoff — disabled

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: retry_test
    +        uri: http://localhost:8080/flakey
    +        predicates:
    +        - Host=*.retry.com
    +        filters:
    +        - name: Retry
    +          args:
    +            retries: 3
    +            statuses: BAD_GATEWAY
    +            methods: GET,POST
    +            backoff:
    +              firstBackoff: 10ms
    +              maxBackoff: 50ms
    +              factor: 2
    +              basedOnPreviousValue: false

    +

    [Note]Note

    When using the retry filter with a forward: prefixed URL, the target endpoint should be written carefully so that in case of an error it does not do anything that could result in a response being sent to the client and committed. For example, if the target endpoint is an annotated controller, the target controller method should not return ResponseEntity with an error status code. Instead it should throw an Exception, or signal an error, e.g. via a Mono.error(ex) return value, which the retry filter can be configured to handle by retrying.

    [Warning]Warning

    When using the retry filter with any HTTP method with a body, the body will be cached and the gateway will become memory constrained. The body is cached in a request attribute defined by ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR. The type of the object is a org.springframework.core.io.buffer.DataBuffer.

    115.25 RequestSize GatewayFilter Factory

    The RequestSize GatewayFilter Factory can restrict a request from reaching the downstream service , when the request size is greater than the permissible limit. The filter takes a maxSize parameter which is the permissible size limit of the request. The maxSize is a `DataSize type, so values can be defined as a number followed by an optional DataUnit suffix such as 'KB' or 'MB'. The default is 'B' for bytes.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: request_size_route
    +      uri: http://localhost:8080/upload
    +      predicates:
    +      - Path=/upload
    +      filters:
    +      - name: RequestSize
    +        args:
    +          maxSize: 5000000

    +

    The RequestSize GatewayFilter Factory set the response status as 413 Payload Too Large with a additional header errorMessage when the Request is rejected due to size. Following is an example of such an errorMessage .

    errorMessage : Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB

    [Note]Note

    The default Request size will be set to 5 MB if not provided as filter argument in route definition.

    115.26 Modify Request Body GatewayFilter Factory

    This filter is considered BETA and the API may change in the future

    The ModifyRequestBody filter can be used to modify the request body before it is sent downstream by the Gateway.

    [Note]Note

    This filter can only be configured using the Java DSL

    @Bean
    +public RouteLocator routes(RouteLocatorBuilder builder) {
    +    return builder.routes()
    +        .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
    +            .filters(f -> f.prefixPath("/httpbin")
    +                .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
    +                    (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
    +        .build();
    +}
    +
    +static class Hello {
    +    String message;
    +
    +    public Hello() { }
    +
    +    public Hello(String message) {
    +        this.message = message;
    +    }
    +
    +    public String getMessage() {
    +        return message;
    +    }
    +
    +    public void setMessage(String message) {
    +        this.message = message;
    +    }
    +}

    115.27 Modify Response Body GatewayFilter Factory

    This filter is considered BETA and the API may change in the future

    The ModifyResponseBody filter can be used to modify the response body before it is sent back to the Client.

    [Note]Note

    This filter can only be configured using the Java DSL

    @Bean
    +public RouteLocator routes(RouteLocatorBuilder builder) {
    +    return builder.routes()
    +        .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
    +            .filters(f -> f.prefixPath("/httpbin")
    +        		.modifyResponseBody(String.class, String.class,
    +        		    (exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri)
    +        .build();
    +}

    115.28 Default Filters

    If you would like to add a filter and apply it to all routes you can use spring.cloud.gateway.default-filters. +This property takes a list of filters

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      default-filters:
    +      - AddResponseHeader=X-Response-Default-Foo, Default-Bar
    +      - PrefixPath=/httpbin

    +

    116. Global Filters

    The GlobalFilter interface has the same signature as GatewayFilter. These are special filters that are conditionally applied to all routes. (This interface and usage are subject to change in future milestones).

    116.1 Combined Global Filter and GatewayFilter Ordering

    When a request comes in (and matches a Route) the Filtering Web Handler will add all instances of GlobalFilter and all route specific instances of GatewayFilter to a filter chain. This combined filter chain is sorted by the org.springframework.core.Ordered interface, which can be set by implementing the getOrder() method.

    As Spring Cloud Gateway distinguishes between "pre" and "post" phases for filter logic execution (see: How it Works), the filter with the highest precedence will be the first in the "pre"-phase and the last in the "post"-phase.

    ExampleConfiguration.java.  +

    @Bean
    +public GlobalFilter customFilter() {
    +    return new CustomGlobalFilter();
    +}
    +
    +public class CustomGlobalFilter implements GlobalFilter, Ordered {
    +
    +    @Override
    +    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    +        log.info("custom global filter");
    +        return chain.filter(exchange);
    +    }
    +
    +    @Override
    +    public int getOrder() {
    +        return -1;
    +    }
    +}

    +

    116.2 Forward Routing Filter

    The ForwardRoutingFilter looks for a URI in the exchange attribute ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a forward scheme (ie forward:///localendpoint), it will use the Spring DispatcherHandler to handler the request. The path part of the request URL will be overridden with the path in the forward URL. The unmodified original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute.

    116.3 LoadBalancerClient Filter

    The LoadBalancerClientFilter looks for a URI in the exchange attribute ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a lb scheme (ie lb://myservice), it will use the Spring Cloud LoadBalancerClient to resolve the name (myservice in the previous example) to an actual host and port and replace the URI in the same attribute. The unmodified original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute. The filter will also look in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR attribute to see if it equals lb and then the same rules apply.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: myRoute
    +        uri: lb://service
    +        predicates:
    +        - Path=/service/**

    +

    [Note]Note

    By default when a service instance cannot be found in the LoadBalancer a 503 will be returned. +You can configure the Gateway to return a 404 by setting spring.cloud.gateway.loadbalancer.use404=true.

    [Note]Note

    The isSecure value of the ServiceInstance returned from the LoadBalancer will override +the scheme specified in the request made to the Gateway. For example, if the request comes into the Gateway over HTTPS +but the ServiceInstance indicates it is not secure, then the downstream request will be made over +HTTP. The opposite situation can also apply. However if GATEWAY_SCHEME_PREFIX_ATTR is specified for the +route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the +route URL will override the ServiceInstance configuration.

    [Warning]Warning

    LoadBalancerClientFilter uses a blocking Ribbon LoadBalancerClient under the hood. +We suggest you use ReactiveLoadBalancerClientFilter instead. +You can switch to using it by adding org.springframework.cloud:spring-cloud-loadbalancer dependency to your project +and setting the value of the spring.cloud.loadbalancer.ribbon.enabled to false.

    116.4 ReactiveLoadBalancerClientFilter

    The ReactiveLoadBalancerClientFilter looks for a URI in the exchange attribute +ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a lb scheme (ie lb://myservice), +it will use the Spring Cloud ReactorLoadBalancer to resolve the name (myservice in the previous example) +to an actual host and port and replace the URI in the same attribute. The unmodified +original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute. +The filter will also look in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR attribute to see if it equals +lb and then the same rules apply.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: myRoute
    +        uri: lb://service
    +        predicates:
    +        - Path=/service/**

    +

    [Note]Note

    By default when a service instance cannot be found by the ReactorLoadBalancer, a 503 will be returned. +You can configure the Gateway to return a 404 by setting spring.cloud.gateway.loadbalancer.use404=true.

    [Note]Note

    The isSecure value of the ServiceInstance returned from the ReactiveLoadBalancerClientFilter will override +the scheme specified in the request made to the Gateway. For example, if the request comes into the Gateway over HTTPS +but the ServiceInstance indicates it is not secure, then the downstream request will be made over +HTTP. The opposite situation can also apply. However if GATEWAY_SCHEME_PREFIX_ATTR is specified for the +route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the +route URL will override the ServiceInstance configuration.

    116.5 Netty Routing Filter

    The Netty Routing Filter runs if the url located in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute has a http or https scheme. It uses the Netty HttpClient to make the downstream proxy request. The response is put in the ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR exchange attribute for use in a later filter. (There is an experimental WebClientHttpRoutingFilter that performs the same function, but does not require netty)

    116.6 Netty Write Response Filter

    The NettyWriteResponseFilter runs if there is a Netty HttpClientResponse in the ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR exchange attribute. It is run after all other filters have completed and writes the proxy response back to the gateway client response. (There is an experimental WebClientWriteResponseFilter that performs the same function, but does not require netty)

    116.7 RouteToRequestUrl Filter

    The RouteToRequestUrlFilter runs if there is a Route object in the ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR exchange attribute. It creates a new URI, based off of the request URI, but updated with the URI attribute of the Route object. The new URI is placed in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute.

    If the URI has a scheme prefix, such as lb:ws://serviceid, the lb scheme is stripped from the URI and placed in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR for use later in the filter chain.

    116.8 Websocket Routing Filter

    The Websocket Routing Filter runs if the url located in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute has a ws or wss scheme. It uses the Spring Web Socket infrastructure to forward the Websocket request downstream.

    Websockets may be load-balanced by prefixing the URI with lb, such as lb:ws://serviceid.

    [Note]Note

    If you are using SockJS as a fallback over normal http, you should configure a normal HTTP route as well as the Websocket Route.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      # SockJS route
    +      - id: websocket_sockjs_route
    +        uri: http://localhost:3001
    +        predicates:
    +        - Path=/websocket/info/**
    +      # Normal Websocket route
    +      - id: websocket_route
    +        uri: ws://localhost:3001
    +        predicates:
    +        - Path=/websocket/**

    +

    116.9 Gateway Metrics Filter

    To enable Gateway Metrics add spring-boot-starter-actuator as a project dependency. Then, by default, the Gateway Metrics Filter runs as long as the property spring.cloud.gateway.metrics.enabled is not set to false. This filter adds a timer metric named "gateway.requests" with the following tags:

    • routeId: The route id
    • routeUri: The URI that the API will be routed to
    • outcome: Outcome as classified by HttpStatus.Series
    • status: Http Status of the request returned to the client
    • httpStatusCode: Http Status of the request returned to the client
    • httpMethod: The Http method used for the request

    These metrics are then available to be scraped from /actuator/metrics/gateway.requests and can be easily integrated with Prometheus to create a Grafana dashboard.

    [Note]Note

    To enable the prometheus endpoint add micrometer-registry-prometheus as a project dependency.

    116.10 Marking An Exchange As Routed

    After the Gateway has routed a ServerWebExchange it will mark that exchange as "routed" by adding gatewayAlreadyRouted +to the exchange attributes. Once a request has been marked as routed, other routing filters will not route the request again, +essentially skipping the filter. There are convenience methods that you can use to mark an exchange as routed +or check if an exchange has already been routed.

    • ServerWebExchangeUtils.isAlreadyRouted takes a ServerWebExchange object and checks if it has been "routed"
    • ServerWebExchangeUtils.setAlreadyRouted takes a ServerWebExchange object and marks it as "routed"

    117. HttpHeadersFilters

    HttpHeadersFilters are applied to requests before sending them downstream, such as in the NettyRoutingFilter.

    117.1 Forwarded Headers Filter

    The Forwarded Headers Filter creates a Forwarded header to send to the downstream service. It adds the Host header, scheme and port of the current request to any existing Forwarded header.

    117.2 RemoveHopByHop Headers Filter

    The RemoveHopByHop Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the IETF.

    The default removed headers are:

    • Connection
    • Keep-Alive
    • Proxy-Authenticate
    • Proxy-Authorization
    • TE
    • Trailer
    • Transfer-Encoding
    • Upgrade

    To change this, set the spring.cloud.gateway.filter.remove-non-proxy-headers.headers property to the list of header names to remove.

    117.3 XForwarded Headers Filter

    The XForwarded Headers Filter creates various a X-Forwarded-* headers to send to the downstream service. It users the Host header, scheme, port and path of the current request to create the various headers.

    Creating of individual headers can be controlled by the following boolean properties (defaults to true):

    • spring.cloud.gateway.x-forwarded.for.enabled
    • spring.cloud.gateway.x-forwarded.host.enabled
    • spring.cloud.gateway.x-forwarded.port.enabled
    • spring.cloud.gateway.x-forwarded.proto.enabled
    • spring.cloud.gateway.x-forwarded.prefix.enabled

    Appending multiple headers can be controlled by the following boolean properties (defaults to true):

    • spring.cloud.gateway.x-forwarded.for.append
    • spring.cloud.gateway.x-forwarded.host.append
    • spring.cloud.gateway.x-forwarded.port.append
    • spring.cloud.gateway.x-forwarded.proto.append
    • spring.cloud.gateway.x-forwarded.prefix.append

    118. TLS / SSL

    The Gateway can listen for requests on https by following the usual Spring server configuration. Example:

    application.yml.  +

    server:
    +  ssl:
    +    enabled: true
    +    key-alias: scg
    +    key-store-password: scg1234
    +    key-store: classpath:scg-keystore.p12
    +    key-store-type: PKCS12

    +

    Gateway routes can be routed to both http and https backends. If routing to a https backend then the Gateway can be configured to trust all downstream certificates with the following configuration:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      httpclient:
    +        ssl:
    +          useInsecureTrustManager: true

    +

    Using an insecure trust manager is not suitable for production. For a production deployment the Gateway can be configured with a set of known certificates that it can trust with the follwing configuration:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      httpclient:
    +        ssl:
    +          trustedX509Certificates:
    +          - cert1.pem
    +          - cert2.pem

    +

    If the Spring Cloud Gateway is not provisioned with trusted certificates the default trust store is used (which can be overriden with system property javax.net.ssl.trustStore).

    118.1 TLS Handshake

    The Gateway maintains a client pool that it uses to route to backends. When communicating over https the client initiates a TLS handshake. A number of timeouts are assoicated with this handshake. These timeouts can be configured (defaults shown):

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      httpclient:
    +        ssl:
    +          handshake-timeout-millis: 10000
    +          close-notify-flush-timeout-millis: 3000
    +          close-notify-read-timeout-millis: 0

    +

    119. Configuration

    Configuration for Spring Cloud Gateway is driven by a collection of RouteDefinitionLocators.

    RouteDefinitionLocator.java.  +

    public interface RouteDefinitionLocator {
    +	Flux<RouteDefinition> getRouteDefinitions();
    +}

    +

    By default, a PropertiesRouteDefinitionLocator loads properties using Spring Boot’s @ConfigurationProperties mechanism.

    The configuration examples above all use a shortcut notation that uses positional arguments rather than named ones. The two examples below are equivalent:

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      routes:
    +      - id: setstatus_route
    +        uri: https://example.org
    +        filters:
    +        - name: SetStatus
    +          args:
    +            status: 401
    +      - id: setstatusshortcut_route
    +        uri: https://example.org
    +        filters:
    +        - SetStatus=401

    +

    For some usages of the gateway, properties will be adequate, but some production use cases will benefit from loading configuration from an external source, such as a database. Future milestone versions will have RouteDefinitionLocator implementations based off of Spring Data Repositories such as: Redis, MongoDB and Cassandra.

    119.1 Fluent Java Routes API

    To allow for simple configuration in Java, there is a fluent API defined in the RouteLocatorBuilder bean.

    GatewaySampleApplication.java.  +

    // static imports from GatewayFilters and RoutePredicates
    +@Bean
    +public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) {
    +    return builder.routes()
    +            .route(r -> r.host("**.abc.org").and().path("/image/png")
    +                .filters(f ->
    +                        f.addResponseHeader("X-TestHeader", "foobar"))
    +                .uri("http://httpbin.org:80")
    +            )
    +            .route(r -> r.path("/image/webp")
    +                .filters(f ->
    +                        f.addResponseHeader("X-AnotherHeader", "baz"))
    +                .uri("http://httpbin.org:80")
    +            )
    +            .route(r -> r.order(-1)
    +                .host("**.throttle.org").and().path("/get")
    +                .filters(f -> f.filter(throttle.apply(1,
    +                        1,
    +                        10,
    +                        TimeUnit.SECONDS)))
    +                .uri("http://httpbin.org:80")
    +            )
    +            .build();
    +}

    +

    This style also allows for more custom predicate assertions. The predicates defined by RouteDefinitionLocator beans are combined using logical and. By using the fluent Java API, you can use the and(), or() and negate() operators on the Predicate class.

    119.2 DiscoveryClient Route Definition Locator

    The Gateway can be configured to create routes based on services registered with a DiscoveryClient compatible service registry.

    To enable this, set spring.cloud.gateway.discovery.locator.enabled=true and make sure a DiscoveryClient implementation is on the classpath and enabled (such as Netflix Eureka, Consul or Zookeeper).

    119.2.1 Configuring Predicates and Filters For DiscoveryClient Routes

    By default the Gateway defines a single predicate and filter for routes created via a DiscoveryClient.

    The default predicate is a path predicate defined with the pattern /serviceId/**, where serviceId is +the id of the service from the DiscoveryClient.

    The default filter is rewrite path filter with the regex /serviceId/(?<remaining>.*) and the replacement +/${remaining}. This just strips the service id from the path before the request is sent +downstream.

    If you would like to customize the predicates and/or filters used by the DiscoveryClient routes you can do so +by setting spring.cloud.gateway.discovery.locator.predicates[x] and spring.cloud.gateway.discovery.locator.filters[y]. +When doing so you need to make sure to include the default predicate and filter above, if you want to retain +that functionality. Below is an example of what this looks like.

    application.properties.  +

    spring.cloud.gateway.discovery.locator.predicates[0].name: Path
    +spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'"
    +spring.cloud.gateway.discovery.locator.predicates[1].name: Host
    +spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'"
    +spring.cloud.gateway.discovery.locator.filters[0].name: Hystrix
    +spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId
    +spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath
    +spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/(?<remaining>.*)'"
    +spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'"

    +

    120. Reactor Netty Access Logs

    To enable Reactor Netty access logs, set -Dreactor.netty.http.server.accessLogEnabled=true. (It must be a Java System Property, not a Spring Boot property).

    The logging system can be configured to have a separate access log file. Below is an example logback configuration:

    logback.xml.  +

        <appender name="accessLog" class="ch.qos.logback.core.FileAppender">
    +        <file>access_log.log</file>
    +        <encoder>
    +            <pattern>%msg%n</pattern>
    +        </encoder>
    +    </appender>
    +    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    +        <appender-ref ref="accessLog" />
    +    </appender>
    +
    +    <logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
    +        <appender-ref ref="async"/>
    +    </logger>

    +

    121. CORS Configuration

    The gateway can be configured to control CORS behavior. The "global" CORS configuration is a map of URL patterns to Spring Framework CorsConfiguration.

    application.yml.  +

    spring:
    +  cloud:
    +    gateway:
    +      globalcors:
    +        corsConfigurations:
    +          '[/**]':
    +            allowedOrigins: "https://docs.spring.io"
    +            allowedMethods:
    +            - GET

    +

    In the example above, CORS requests will be allowed from requests that originate from docs.spring.io for all GET requested paths.

    To provide the same CORS configuration to requests that are not handled by some gateway route predicate, set the property spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping equal to true. This is useful when trying to support CORS preflight requests and your route predicate doesn’t evalute to true because the http method is options.

    122. Actuator API

    The /gateway actuator endpoint allows to monitor and interact with a Spring Cloud Gateway application. To be remotely accessible, the endpoint has to be enabled and exposed via HTTP or JMX in the application properties.

    application.properties.  +

    management.endpoint.gateway.enabled=true # default value
    +management.endpoints.web.exposure.include=gateway

    +

    122.1 Verbose Actuator Format

    A new, more verbose format has been added to Gateway. This adds more detail to each route allowing to view the predicates and filters associated to each route along with any configuration that is available.

    /actuator/gateway/routes

    [
    +  {
    +    "predicate": "(Hosts: [**.addrequestheader.org] && Paths: [/headers], match trailing slash: true)",
    +    "route_id": "add_request_header_test",
    +    "filters": [
    +      "[[AddResponseHeader X-Response-Default-Foo = 'Default-Bar'], order = 1]",
    +      "[[AddRequestHeader X-Request-Foo = 'Bar'], order = 1]",
    +      "[[PrefixPath prefix = '/httpbin'], order = 2]"
    +    ],
    +    "uri": "lb://testservice",
    +    "order": 0
    +  }
    +]

    To enable this feature, set the following property:

    application.properties.  +

    spring.cloud.gateway.actuator.verbose.enabled=true

    +

    This will default to true in a future release.

    122.2 Retrieving route filters

    122.2.1 Global Filters

    To retrieve the global filters applied to all routes, make a GET request to /actuator/gateway/globalfilters. The resulting response is similar to the following:

    {
    +  "org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5": 10100,
    +  "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4f6fd101": 10000,
    +  "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@32d22650": -1,
    +  "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@106459d9": 2147483647,
    +  "org.springframework.cloud.gateway.filter.NettyRoutingFilter@1fbd5e0": 2147483647,
    +  "org.springframework.cloud.gateway.filter.ForwardPathFilter@33a71d23": 0,
    +  "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@135064ea": 2147483637,
    +  "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@23c05889": 2147483646
    +}

    The response contains details of the global filters in place. For each global filter is provided the string representation of the filter object (e.g., org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5) and the corresponding order in the filter chain.

    122.2.2 Route Filters

    To retrieve the GatewayFilter factories applied to routes, make a GET request to /actuator/gateway/routefilters. The resulting response is similar to the following:

    {
    +  "[AddRequestHeaderGatewayFilterFactory@570ed9c configClass = AbstractNameValueGatewayFilterFactory.NameValueConfig]": null,
    +  "[SecureHeadersGatewayFilterFactory@fceab5d configClass = Object]": null,
    +  "[SaveSessionGatewayFilterFactory@4449b273 configClass = Object]": null
    +}

    The response contains details of the GatewayFilter factories applied to any particular route. For each factory is provided the string representation of the corresponding object (e.g., [SecureHeadersGatewayFilterFactory@fceab5d configClass = Object]). Note that the null value is due to an incomplete implementation of the endpoint controller, for that it tries to set the order of the object in the filter chain, which does not apply to a GatewayFilter factory object.

    122.3 Refreshing the route cache

    To clear the routes cache, make a POST request to /actuator/gateway/refresh. The request returns a 200 without response body.

    122.4 Retrieving the routes defined in the gateway

    To retrieve the routes defined in the gateway, make a GET request to /actuator/gateway/routes. The resulting response is similar to the following:

    [{
    +  "route_id": "first_route",
    +  "route_object": {
    +    "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@1e9d7e7d",
    +    "filters": [
    +      "OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory$$Lambda$436/674480275@6631ef72, order=0}"
    +    ]
    +  },
    +  "order": 0
    +},
    +{
    +  "route_id": "second_route",
    +  "route_object": {
    +    "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@cd8d298",
    +    "filters": []
    +  },
    +  "order": 0
    +}]

    The response contains details of all the routes defined in the gateway. The following table describes the structure of each element (i.e., a route) of the response.

    PathTypeDescription

    route_id

    String

    The route id.

    route_object.predicate

    Object

    The route predicate.

    route_object.filters

    Array

    The GatewayFilter factories applied to the route.

    order

    Number

    The route order.

    122.5 Retrieving information about a particular route

    To retrieve information about a single route, make a GET request to /actuator/gateway/routes/{id} (e.g., /actuator/gateway/routes/first_route). The resulting response is similar to the following:

    {
    +  "id": "first_route",
    +  "predicates": [{
    +    "name": "Path",
    +    "args": {"_genkey_0":"/first"}
    +  }],
    +  "filters": [],
    +  "uri": "https://www.uri-destination.org",
    +  "order": 0
    +}]

    The following table describes the structure of the response.

    PathTypeDescription

    id

    String

    The route id.

    predicates

    Array

    The collection of route predicates. Each item defines the name and the arguments of a given predicate.

    filters

    Array

    The collection of filters applied to the route.

    uri

    String

    The destination URI of the route.

    order

    Number

    The route order.

    122.6 Creating and deleting a particular route

    To create a route, make a POST request to /gateway/routes/{id_route_to_create} with a JSON body that specifies the fields of the route (see the previous subsection).

    To delete a route, make a DELETE request to /gateway/routes/{id_route_to_delete}.

    122.7 Recap: list of all endpoints

    The table below summarises the Spring Cloud Gateway actuator endpoints. Note that each endpoint has /actuator/gateway as the base-path.

    IDHTTP MethodDescription

    globalfilters

    GET

    Displays the list of global filters applied to the routes.

    routefilters

    GET

    Displays the list of GatewayFilter factories applied to a particular route.

    refresh

    POST

    Clears the routes cache.

    routes

    GET

    Displays the list of routes defined in the gateway.

    routes/{id}

    GET

    Displays information about a particular route.

    routes/{id}

    POST

    Add a new route to the gateway.

    routes/{id}

    DELETE

    Remove an existing route from the gateway.

    123. Troubleshooting

    123.1 Log Levels

    Below are some useful loggers that contain valuable trouble shooting infomration at the DEBUG and TRACE levels.

    • org.springframework.cloud.gateway
    • org.springframework.http.server.reactive
    • org.springframework.web.reactive
    • org.springframework.boot.autoconfigure.web
    • reactor.netty
    • redisratelimiter

    123.2 Wiretap

    The Reactor Netty HttpClient and HttpServer can have wiretap enabled. When combined +with setting the reactor.netty log level to DEBUG or TRACE will enable logging of +information such as headers and bodies sent and received across the wire. To enable this, +set spring.cloud.gateway.httpserver.wiretap=true and/or +spring.cloud.gateway.httpclient.wiretap=true for the HttpServer and HttpClient +respectively.

    124. Developer Guide

    These are basic guides to writing some custom components of the gateway.

    124.1 Writing Custom Route Predicate Factories

    In order to write a Route Predicate you will need to implement RoutePredicateFactory. There is an abstract class called AbstractRoutePredicateFactory which you can extend.

    MyRoutePredicateFactory.java.  +

    public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {
    +
    +    public MyRoutePredicateFactory() {
    +        super(Config.class);
    +    }
    +
    +    @Override
    +    public Predicate<ServerWebExchange> apply(Config config) {
    +        // grab configuration from Config object
    +        return exchange -> {
    +            //grab the request
    +            ServerHttpRequest request = exchange.getRequest();
    +            //take information from the request to see if it
    +            //matches configuration.
    +            return matches(config, request);
    +        };
    +    }
    +
    +    public static class Config {
    +        //Put the configuration properties for your filter here
    +    }
    +
    +}

    +

    124.2 Writing Custom GatewayFilter Factories

    In order to write a GatewayFilter you will need to implement GatewayFilterFactory. There is an abstract class called AbstractGatewayFilterFactory which you can extend.

    PreGatewayFilterFactory.java.  +

    public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory<PreGatewayFilterFactory.Config> {
    +
    +	public PreGatewayFilterFactory() {
    +		super(Config.class);
    +	}
    +
    +	@Override
    +	public GatewayFilter apply(Config config) {
    +		// grab configuration from Config object
    +		return (exchange, chain) -> {
    +			//If you want to build a "pre" filter you need to manipulate the
    +			//request before calling chain.filter
    +			ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
    +			//use builder to manipulate the request
    +			return chain.filter(exchange.mutate().request(request).build());
    +		};
    +	}
    +
    +	public static class Config {
    +		//Put the configuration properties for your filter here
    +	}
    +
    +}

    +

    PostGatewayFilterFactory.java.  +

    public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory<PostGatewayFilterFactory.Config> {
    +
    +	public PostGatewayFilterFactory() {
    +		super(Config.class);
    +	}
    +
    +	@Override
    +	public GatewayFilter apply(Config config) {
    +		// grab configuration from Config object
    +		return (exchange, chain) -> {
    +			return chain.filter(exchange).then(Mono.fromRunnable(() -> {
    +				ServerHttpResponse response = exchange.getResponse();
    +				//Manipulate the response in some way
    +			}));
    +		};
    +	}
    +
    +	public static class Config {
    +		//Put the configuration properties for your filter here
    +	}
    +
    +}

    +

    124.3 Writing Custom Global Filters

    In order to write a custom global filter, you will need to implement GlobalFilter interface. This will apply the filter to all requests.

    Example of how to set up a Global Pre and Post filter, respectively

    @Bean
    +public GlobalFilter customGlobalFilter() {
    +    return (exchange, chain) -> exchange.getPrincipal()
    +        .map(Principal::getName)
    +        .defaultIfEmpty("Default User")
    +        .map(userName -> {
    +          //adds header to proxied request
    +          exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build();
    +          return exchange;
    +        })
    +        .flatMap(chain::filter);
    +}
    +
    +@Bean
    +public GlobalFilter customGlobalPostFilter() {
    +    return (exchange, chain) -> chain.filter(exchange)
    +        .then(Mono.just(exchange))
    +        .map(serverWebExchange -> {
    +          //adds header to response
    +          serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER",
    +              HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked": "It did not work");
    +          return serverWebExchange;
    +        })
    +        .then();
    +}

    125. Building a Simple Gateway Using Spring MVC or Webflux

    [Warning]Warning

    The following describes an alternative style gateway. None of the prior documentation applies to what follows.

    Spring Cloud Gateway provides a utility object called ProxyExchange which you can use inside a regular Spring web handler as a method parameter. It supports basic downstream HTTP exchanges via methods that mirror the HTTP verbs. With MVC it also supports forwarding to a local handler via the forward() method. To use the ProxyExchange just include the right module in your classpath (either spring-cloud-gateway-mvc or spring-cloud-gateway-webflux).

    MVC example (proxying a request to "/test" downstream to a remote server):

    @RestController
    +@SpringBootApplication
    +public class GatewaySampleApplication {
    +
    +	@Value("${remote.home}")
    +	private URI home;
    +
    +	@GetMapping("/test")
    +	public ResponseEntity<?> proxy(ProxyExchange<byte[]> proxy) throws Exception {
    +		return proxy.uri(home.toString() + "/image/png").get();
    +	}
    +
    +}

    The same thing with Webflux:

    @RestController
    +@SpringBootApplication
    +public class GatewaySampleApplication {
    +
    +	@Value("${remote.home}")
    +	private URI home;
    +
    +	@GetMapping("/test")
    +	public Mono<ResponseEntity<?>> proxy(ProxyExchange<byte[]> proxy) throws Exception {
    +		return proxy.uri(home.toString() + "/image/png").get();
    +	}
    +
    +}

    There are convenience methods on the ProxyExchange to enable the handler method to discover and enhance the URI path of the incoming request. For example you might want to extract the trailing elements of a path to pass them downstream:

    @GetMapping("/proxy/path/**")
    +public ResponseEntity<?> proxyPath(ProxyExchange<byte[]> proxy) throws Exception {
    +  String path = proxy.path("/proxy/path/");
    +  return proxy.uri(home.toString() + "/foos/" + path).get();
    +}

    All the features of Spring MVC or Webflux are available to Gateway handler methods. So you can inject request headers and query parameters, for instance, and you can constrain the incoming requests with declarations in the mapping annotation. See the documentation for @RequestMapping in Spring MVC for more details of those features.

    Headers can be added to the downstream response using the header() methods on ProxyExchange.

    You can also manipulate response headers (and anything else you like in the response) by adding a mapper to the get() etc. method. The mapper is a Function that takes the incoming ResponseEntity and converts it to an outgoing one.

    First class support is provided for "sensitive" headers ("cookie" and "authorization" by default) which are not passed downstream, and for "proxy" headers (x-forwarded-*).

    Part XVI. Spring Cloud Function

    Mark Fisher, Dave Syer, Oleg Zhurakousky

    126. Introduction

    Spring Cloud Function is a project with the following high-level goals:

    • Promote the implementation of business logic via functions.
    • Decouple the development lifecycle of business logic from any specific runtime target so that the same code can run as a web endpoint, a stream processor, or a task.
    • Support a uniform programming model across serverless providers, as well as the ability to run standalone (locally or in a PaaS).
    • Enable Spring Boot features (auto-configuration, dependency injection, metrics) on serverless providers.

    It abstracts away all of the transport details and +infrastructure, allowing the developer to keep all the familiar tools +and processes, and focus firmly on business logic.

    Here’s a complete, executable, testable Spring Boot application +(implementing a simple string manipulation):

    @SpringBootApplication
    +public class Application {
    +
    +  @Bean
    +  public Function<Flux<String>, Flux<String>> uppercase() {
    +    return flux -> flux.map(value -> value.toUpperCase());
    +  }
    +
    +  public static void main(String[] args) {
    +    SpringApplication.run(Application.class, args);
    +  }
    +}

    It’s just a Spring Boot application, so it can be built, run and +tested, locally and in a CI build, the same way as any other Spring +Boot application. The Function is from java.util and Flux is a +Reactive Streams Publisher from +Project Reactor. The function can be +accessed over HTTP or messaging.

    Spring Cloud Function has 4 main features:

    1. Wrappers for @Beans of type Function, Consumer and +Supplier, exposing them to the outside world as either HTTP +endpoints and/or message stream listeners/publishers with RabbitMQ, Kafka etc.
    2. Compiling strings which are Java function bodies into bytecode, and +then turning them into @Beans that can be wrapped as above.
    3. Deploying a JAR file containing such an application context with an +isolated classloader, so that you can pack them together in a single +JVM.
    4. Adapters for AWS Lambda, Azure, Apache OpenWhisk and possibly other "serverless" service providers.
    [Note]Note

    Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github.

    127. Getting Started

    Build from the command line (and "install" the samples):

    $ ./mvnw clean install

    (If you like to YOLO add -DskipTests.)

    Run one of the samples, e.g.

    $ java -jar spring-cloud-function-samples/function-sample/target/*.jar

    This runs the app and exposes its functions over HTTP, so you can +convert a string to uppercase, like this:

    $ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d Hello
    +HELLO

    You can convert multiple strings (a Flux<String>) by separating them +with new lines

    $ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d 'Hello
    +> World'
    +HELLOWORLD

    (You can use QJ in a terminal to insert a new line in a literal +string like that.)

    128. Building and Running a Function

    The sample @SpringBootApplication above has a function that can be +decorated at runtime by Spring Cloud Function to be an HTTP endpoint, +or a Stream processor, for instance with RabbitMQ, Apache Kafka or +JMS.

    The @Beans can be Function, Consumer or Supplier (all from +java.util), and their parametric types can be String or POJO.

    Functions can also be of Flux<String> or Flux<Pojo> and Spring +Cloud Function takes care of converting the data to and from the +desired types, as long as it comes in as plain text or (in the case of +the POJO) JSON. There is also support for Message<Pojo> where the +message headers are copied from the incoming event, depending on the +adapter. The web adapter also supports conversion from form-encoded +data to a Map, and if you are using the function with Spring Cloud +Stream then all the conversion and coercion features for message +payloads will be applicable as well.

    Functions can be grouped together in a single application, or deployed +one-per-jar. It’s up to the developer to choose. An app with multiple +functions can be deployed multiple times in different "personalities", +exposing different functions over different physical transports.

    129. Function Catalog and Flexible Function Signatures

    One of the main features of Spring Cloud Function is to adapt and support a range of type signatures for user-defined functions, +while providing a consistent execution model. +That’s why all user defined functions are transformed into a canonical representation by FunctionCatalog, using primitives +defined by the Project Reactor (i.e., Flux<T> and Mono<T>). +Users can supply a bean of type Function<String,String>, for instance, and the FunctionCatalog will wrap it into a +Function<Flux<String>,Flux<String>>.

    Using Reactor based primitives not only helps with the canonical representation of user defined functions, but it also +facilitates a more robust and flexible(reactive) execution model.

    While users don’t normally have to care about the FunctionCatalog at all, it is useful to know what +kind of functions are supported in user code.

    129.1 Java 8 function support

    Generally speaking users can expect that if they write a function for +a plain old Java type (or primitive wrapper), then the function +catalog will wrap it to a Flux of the same type. If the user writes +a function using Message (from spring-messaging) it will receive and +transmit headers from any adapter that supports key-value metadata +(e.g. HTTP headers). Here are the details.

    User FunctionCatalog Registration 

    Function<S,T>

    Function<Flux<S>, Flux<T>>

     

    Function<Message<S>,Message<T>>

    Function<Flux<Message<S>>, Flux<Message<T>>>

     

    Function<Flux<S>, Flux<T>>

    Function<Flux<S>, Flux<T>> (pass through)

     

    Supplier<T>

    Supplier<Flux<T>>

     

    Supplier<Flux<T>>

    Supplier<Flux<T>>

     

    Consumer<T>

    Function<Flux<T>, Mono<Void>>

     

    Consumer<Message<T>>

    Function<Flux<Message<T>>, Mono<Void>>

     

    Consumer<Flux<T>>

    Consumer<Flux<T>>

     

    Consumer is a little bit special because it has a void return type, +which implies blocking, at least potentially. Most likely you will not +need to write Consumer<Flux<?>>, but if you do need to do that, +remember to subscribe to the input flux. If you declare a Consumer +of a non publisher type (which is normal), it will be converted to a +function that returns a publisher, so that it can be subscribed to in +a controlled way.

    129.2 Kotlin Lambda support

    We also provide support for Kotlin lambdas (since v2.0). +Consider the following:

    @Bean
    +open fun kotlinSupplier(): () -> String {
    +    return  { "Hello from Kotlin" }
    +}
    +
    +@Bean
    +open fun kotlinFunction(): (String) -> String {
    +    return  { it.toUpperCase() }
    +}
    +
    +@Bean
    +open fun kotlinConsumer(): (String) -> Unit {
    +    return  { println(it) }
    +}

    The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of +Supplier, Function and Consumer, and thus supported/recognized signatures by the framework. +While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the +same rules for signature transformation outlined in "Java 8 function support" section are applied here as well.

    To enable Kotlin support all you need is to add spring-cloud-function-kotlin module to your classpath which contains the appropriate +autoconfiguration and supporting classes.

    130. Standalone Web Applications

    The spring-cloud-function-web module has autoconfiguration that +activates when it is included in a Spring Boot web application (with +MVC support). There is also a spring-cloud-starter-function-web to +collect all the optional dependencies in case you just want a simple +getting started experience.

    With the web configurations activated your app will have an MVC +endpoint (on "/" by default, but configurable with +spring.cloud.function.web.path) that can be used to access the +functions in the application context. The supported content types are +plain text and JSON.

    MethodPathRequestResponseStatus

    GET

    /{supplier}

    -

    Items from the named supplier

    200 OK

    POST

    /{consumer}

    JSON object or text

    Mirrors input and pushes request body into consumer

    202 Accepted

    POST

    /{consumer}

    JSON array or text with new lines

    Mirrors input and pushes body into consumer one by one

    202 Accepted

    POST

    /{function}

    JSON object or text

    The result of applying the named function

    200 OK

    POST

    /{function}

    JSON array or text with new lines

    The result of applying the named function

    200 OK

    GET

    /{function}/{item}

    -

    Convert the item into an object and return the result of applying the function

    200 OK

    As the table above shows the behaviour of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or Flux), then the response will also contain a single value. +For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream".

    If there is only a single function (consumer etc.) in the catalog, the name in the path is optional. +Composite functions can be addressed using pipes or commas to separate function names (pipes are legal in URL paths, but a bit awkward to type on the command line).

    For cases where there is more then a single function in catalog and you want to map a specific function to the root +path (e.g., "/"), or you want to compose several functions and then map to the root path you can do so by providing +spring.cloud.function.definition property which essentially used by spring-=cloud-function-web module to provide +default mapping for cases where there is some type of a conflict (e.g., more then one function available etc).

    For example,

    --spring.cloud.function.definition=foo|bar

    The above property will compose 'foo' and 'bar' function and map the composed function to the "/" path.

    Functions and consumers that are declared with input and output in Message<?> will see the request headers on the input messages, and the output message headers will be converted to HTTP headers.

    When POSTing text the response format might be different with Spring Boot 2.0 and older versions, depending on the content negotiation (provide content type and accpt headers for the best results).

    131. Standalone Streaming Applications

    To send or receive messages from a broker (such as RabbitMQ or Kafka) you can leverage spring-cloud-stream project and it’s integration with Spring Cloud Function. +Please refer to Spring Cloud Function section of the Spring Cloud Stream reference manual for more details and examples.

    132. Deploying a Packaged Function

    Spring Cloud Function provides a "deployer" library that allows you to launch a jar file (or exploded archive, or set of jar files) with an isolated class loader and expose the functions defined in it. This is quite a powerful tool that would allow you to, for instance, adapt a function to a range of different input-output adapters without changing the target jar file. Serverless platforms often have this kind of feature built in, so you could see it as a building block for a function invoker in such a platform (indeed the Riff Java function invoker uses this library).

    The standard entry point of the API is the Spring configuration annotation @EnableFunctionDeployer. If that is used in a Spring Boot application the deployer kicks in and looks for some configuration to tell it where to find the function jar. At a minimum the user has to provide a function.location which is a URL or resource location for the archive containing the functions. It can optionally use a maven: prefix to locate the artifact via a dependency lookup (see FunctionProperties for complete details). A Spring Boot application is bootstrapped from the jar file, using the MANIFEST.MF to locate a start class, so that a standard Spring Boot fat jar works well, for example. If the target jar can be launched successfully then the result is a function registered in the main application’s FunctionCatalog. The registered function can be applied by code in the main application, even though it was created in an isolated class loader (by deault).

    133. Functional Bean Definitions

    Spring Cloud Function supports a "functional" style of bean declarations for small apps where you need fast startup. The functional style of bean declaration was a feature of Spring Framework 5.0 with significant enhancements in 5.1.

    133.1 Comparing Functional with Traditional Bean Definitions

    Here’s a vanilla Spring Cloud Function application from with the +familiar @Configuration and @Bean declaration style:

    @SpringBootApplication
    +public class DemoApplication {
    +
    +  @Bean
    +  public Function<String, String> uppercase() {
    +    return value -> value.toUpperCase();
    +  }
    +
    +  public static void main(String[] args) {
    +    SpringApplication.run(DemoApplication.class, args);
    +  }
    +
    +}

    You can run the above in a serverless platform, like AWS Lambda or Azure Functions, or you can run it in its own HTTP server just by including spring-cloud-function-starter-web on the classpath. Running the main method would expose an endpoint that you can use to ping that uppercase function:

    $ curl localhost:8080 -d foo
    +FOO

    The web adapter in spring-cloud-function-starter-web uses Spring MVC, so you needed a Servlet container. You can also use Webflux where the default server is netty (even though you can still use Servlet containers if you want to) - just include the spring-cloud-starter-function-webflux dependency instead. The functionality is the same, and the user application code can be used in both.

    Now for the functional beans: the user application code can be recast into "functional" +form, like this:

    @SpringBootConfiguration
    +public class DemoApplication implements ApplicationContextInitializer<GenericApplicationContext> {
    +
    +  public static void main(String[] args) {
    +    FunctionalSpringApplication.run(DemoApplication.class, args);
    +  }
    +
    +  public Function<String, String> uppercase() {
    +    return value -> value.toUpperCase();
    +  }
    +
    +  @Override
    +  public void initialize(GenericApplicationContext context) {
    +    context.registerBean("demo", FunctionRegistration.class,
    +        () -> new FunctionRegistration<>(uppercase())
    +            .type(FunctionType.from(String.class).to(String.class)));
    +  }
    +
    +}

    The main differences are:

    • The main class is an ApplicationContextInitializer.
    • The @Bean methods have been converted to calls to context.registerBean()
    • The @SpringBootApplication has been replaced with +@SpringBootConfiguration to signify that we are not enabling Spring +Boot autoconfiguration, and yet still marking the class as an "entry +point".
    • The SpringApplication from Spring Boot has been replaced with a +FunctionalSpringApplication from Spring Cloud Function (it’s a +subclass).

    The business logic beans that you register in a Spring Cloud Function app are of type FunctionRegistration. This is a wrapper that contains both the function and information about the input and output types. In the @Bean form of the application that information can be derived reflectively, but in a functional bean registration some of it is lost unless we use a FunctionRegistration.

    An alternative to using an ApplicationContextInitializer and FunctionRegistration is to make the application itself implement Function (or Consumer or Supplier). Example (equivalent to the above):

    @SpringBootConfiguration
    +public class DemoApplication implements Function<String, String> {
    +
    +  public static void main(String[] args) {
    +    FunctionalSpringApplication.run(DemoApplication.class, args);
    +  }
    +
    +  @Override
    +  public String uppercase(String value) {
    +    return value.toUpperCase();
    +  }
    +
    +}

    It would also work if you add a separate, standalone class of type Function and register it with the SpringApplication using an alternative form of the run() method. The main thing is that the generic type information is available at runtime through the class declaration.

    The app runs in its own HTTP server if you add spring-cloud-starter-function-webflux (it won’t work with the MVC starter at the moment because the functional form of the embedded Servlet container hasn’t been implemented). The app also runs just fine in AWS Lambda or Azure Functions, and the improvements in startup time are dramatic.

    [Note]Note

    The "lite" web server has some limitations for the range of Function signatures - in particular it doesn’t (yet) support Message input and output, but POJOs and any kind of Publisher should be fine.

    133.2 Testing Functional Applications

    Spring Cloud Function also has some utilities for integration testing that will be very familiar to Spring Boot users. For example, here is an integration test for the HTTP server wrapping the app above:

    @RunWith(SpringRunner.class)
    +@FunctionalSpringBootTest
    +@AutoConfigureWebTestClient
    +public class FunctionalTests {
    +
    +	@Autowired
    +	private WebTestClient client;
    +
    +	@Test
    +	public void words() throws Exception {
    +		client.post().uri("/").body(Mono.just("foo"), String.class).exchange()
    +				.expectStatus().isOk().expectBody(String.class).isEqualTo("FOO");
    +	}
    +
    +}

    This test is almost identical to the one you would write for the @Bean version of the same app - the only difference is the @FunctionalSpringBootTest annotation, instead of the regular @SpringBootTest. All the other pieces, like the @Autowired WebTestClient, are standard Spring Boot features.

    Or you could write a test for a non-HTTP app using just the FunctionCatalog. For example:

    @RunWith(SpringRunner.class)
    +@FunctionalSpringBootTest
    +public class FunctionalTests {
    +
    +	@Autowired
    +	private FunctionCatalog catalog;
    +
    +	@Test
    +	public void words() throws Exception {
    +		Function<Flux<String>, Flux<String>> function = catalog.lookup(Function.class,
    +				"function");
    +		assertThat(function.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO");
    +	}
    +
    +}

    (The FunctionCatalog always returns functions from Flux to Flux, even if the user declares them with a simpler signature.)

    133.3 Limitations of Functional Bean Declaration

    Most Spring Cloud Function apps have a relatively small scope compared to the whole of Spring Boot, so we are able to adapt it to these functional bean definitions easily. If you step outside that limited scope, you can extend your Spring Cloud Function app by switching back to @Bean style configuration, or by using a hybrid approach. If you want to take advantage of Spring Boot autoconfiguration for integrations with external datastores, for example, you will need to use @EnableAutoConfiguration. Your functions can still be defined using the functional declarations if you want (i.e. the "hybrid" style), but in that case you will need to explicitly switch off the "full functional mode" using spring.functional.enabled=false so that Spring Boot can take back control.

    134. Dynamic Compilation

    There is a sample app that uses the function compiler to create a +function from a configuration property. The vanilla "function-sample" +also has that feature. And there are some scripts that you can run to +see the compilation happening at run time. To run these examples, +change into the scripts directory:

    cd scripts

    Also, start a RabbitMQ server locally (e.g. execute rabbitmq-server).

    Start the Function Registry Service:

    ./function-registry.sh

    Register a Function:

    ./registerFunction.sh -n uppercase -f "f->f.map(s->s.toString().toUpperCase())"

    Run a REST Microservice using that Function:

    ./web.sh -f uppercase -p 9000
    +curl -H "Content-Type: text/plain" -H "Accept: text/plain" localhost:9000/uppercase -d foo

    Register a Supplier:

    ./registerSupplier.sh -n words -f "()->Flux.just(\"foo\",\"bar\")"

    Run a REST Microservice using that Supplier:

    ./web.sh -s words -p 9001
    +curl -H "Accept: application/json" localhost:9001/words

    Register a Consumer:

    ./registerConsumer.sh -n print -t String -f "System.out::println"

    Run a REST Microservice using that Consumer:

    ./web.sh -c print -p 9002
    +curl -X POST -H "Content-Type: text/plain" -d foo localhost:9002/print

    Run Stream Processing Microservices:

    First register a streaming words supplier:

    ./registerSupplier.sh -n wordstream -f "()->Flux.interval(Duration.ofMillis(1000)).map(i->\"message-\"+i)"

    Then start the source (supplier), processor (function), and sink (consumer) apps +(in reverse order):

    ./stream.sh -p 9103 -i uppercaseWords -c print
    +./stream.sh -p 9102 -i words -f uppercase -o uppercaseWords
    +./stream.sh -p 9101 -s wordstream -o words

    The output will appear in the console of the sink app (one message per second, converted to uppercase):

    MESSAGE-0
    +MESSAGE-1
    +MESSAGE-2
    +MESSAGE-3
    +MESSAGE-4
    +MESSAGE-5
    +MESSAGE-6
    +MESSAGE-7
    +MESSAGE-8
    +MESSAGE-9
    +...

    135. Serverless Platform Adapters

    As well as being able to run as a standalone process, a Spring Cloud +Function application can be adapted to run one of the existing +serverless platforms. In the project there are adapters for +AWS +Lambda, +Azure, +and +Apache +OpenWhisk. The Oracle Fn platform +has its own Spring Cloud Function adapter. And +Riff supports Java functions and its +Java Function +Invoker acts natively is an adapter for Spring Cloud Function jars.

    135.1 AWS Lambda

    The AWS adapter takes a Spring Cloud Function app and converts it to a form that can run in AWS Lambda.

    135.1.1 Introduction

    The adapter has a couple of generic request handlers that you can use. The most generic is SpringBootStreamHandler, which uses a Jackson ObjectMapper provided by Spring Boot to serialize and deserialize the objects in the function. There is also a SpringBootRequestHandler which you can extend, and provide the input and output types as type parameters (enabling AWS to inspect the class and do the JSON conversions itself).

    If your app has more than one @Bean of type Function etc. then you can choose the one to use by configuring function.name (e.g. as FUNCTION_NAME environment variable in AWS). The functions are extracted from the Spring Cloud FunctionCatalog (searching first for Function then Consumer and finally Supplier).

    135.1.2 Notes on JAR Layout

    You don’t need the Spring Cloud Function Web or Stream adapter at runtime in Lambda, so you might need to exclude those before you create the JAR you send to AWS. A Lambda application has to be shaded, but a Spring Boot standalone application does not, so you can run the same app using 2 separate jars (as per the sample). The sample app creates 2 jar files, one with an aws classifier for deploying in Lambda, and one executable (thin) jar that includes spring-cloud-function-web at runtime. Spring Cloud Function will try and locate a "main class" for you from the JAR file manifest, using the Start-Class attribute (which will be added for you by the Spring Boot tooling if you use the starter parent). If there is no Start-Class in your manifest you can use an environment variable MAIN_CLASS when you deploy the function to AWS.

    135.1.3 Upload

    Build the sample under spring-cloud-function-samples/function-sample-aws and upload the -aws jar file to Lambda. The handler can be example.Handler or org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler (FQN of the class, not a method reference, although Lambda does accept method references).

    ./mvnw -U clean package

    Using the AWS command line tools it looks like this:

    aws lambda create-function --function-name Uppercase --role arn:aws:iam::[USERID]:role/service-role/[ROLE] --zip-file fileb://function-sample-aws/target/function-sample-aws-2.0.0.BUILD-SNAPSHOT-aws.jar --handler org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler --description "Spring Cloud Function Adapter Example" --runtime java8 --region us-east-1 --timeout 30 --memory-size 1024 --publish

    The input type for the function in the AWS sample is a Foo with a single property called "value". So you would need this to test it:

    {
    +  "value": "test"
    +}
    [Note]Note

    The AWS sample app is written in the "functional" style (as an ApplicationContextInitializer). This is much faster on startup in Lambda than the traditional @Bean style, so if you don’t need @Beans (or @EnableAutoConfiguration) it’s a good choice. Warm starts are not affected.

    135.1.4 Platfom Specific Features

    HTTP and API Gateway

    AWS has some platform-specific data types, including batching of messages, which is much more efficient than processing each one individually. To make use of these types you can write a function that depends on those types. Or you can rely on Spring to extract the data from the AWS types and convert it to a Spring Message. To do this you tell AWS that the function is of a specific generic handler type (depending on the AWS service) and provide a bean of type Function<Message<S>,Message<T>>, where S and T are your business data types. If there is more than one bean of type Function you may also need to configure the Spring Boot property function.name to be the name of the target bean (e.g. use FUNCTION_NAME as an environment variable).

    The supported AWS services and generic handler types are listed below:

    ServiceAWS TypesGeneric Handler 

    API Gateway

    APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent

    org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler

     

    Kinesis

    KinesisEvent

    org.springframework.cloud.function.adapter.aws.SpringBootKinesisEventHandler

     

    For example, to deploy behind an API Gateway, use --handler org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler in your AWS command line (in via the UI) and define a @Bean of type Function<Message<Foo>,Message<Bar>> where Foo and Bar are POJO types (the data will be marshalled and unmarshalled by AWS using Jackson).

    135.2 Azure Functions

    The Azure adapter bootstraps a Spring Cloud Function context and channels function calls from the Azure framework into the user functions, using Spring Boot configuration where necessary. Azure Functions has quite a unique, but invasive programming model, involving annotations in user code that are specific to the platform. The easiest way to use it with Spring Cloud is to extend a base class and write a method in it with the @FunctionName annotation which delegates to a base class method.

    This project provides an adapter layer for a Spring Cloud Function application onto Azure. +You can write an app with a single @Bean of type Function and it will be deployable in Azure if you get the JAR file laid out right.

    There is an AzureSpringBootRequestHandler which you must extend, and provide the input and output types as annotated method parameters (enabling Azure to inspect the class and create JSON bindings). The base class has two useful methods (handleRequest and handleOutput) to which you can delegate the actual function call, so mostly the function will only ever have one line.

    Example:

    public class FooHandler extends AzureSpringBootRequestHandler<Foo, Bar> {
    +	@FunctionName("uppercase")
    +	public Bar execute(
    +			@HttpTrigger(name = "req", methods = { HttpMethod.GET,
    +					HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS)
    +                    Foo foo,
    +			ExecutionContext context) {
    +		return handleRequest(foo, context);
    +	}
    +}

    This Azure handler will delegate to a Function<Foo,Bar> bean (or a Function<Publisher<Foo>,Publisher<Bar>>). Some Azure triggers (e.g. @CosmosDBTrigger) result in a input type of List and in that case you can bind to List in the Azure handler, or String (the raw JSON). The List input delegates to a Function with input type Map<String,Object>, or Publisher or List of the same type. The output of the Function can be a List (one-for-one) or a single value (aggregation), and the output binding in the Azure declaration should match.

    If your app has more than one @Bean of type Function etc. then you can choose the one to use by configuring function.name. Or if you make the @FunctionName in the Azure handler method match the function name it should work that way (also for function apps with multiple functions). The functions are extracted from the Spring Cloud FunctionCatalog so the default function names are the same as the bean names.

    135.2.1 Notes on JAR Layout

    You don’t need the Spring Cloud Function Web at runtime in Azure, so you can exclude this before you create the JAR you deploy to Azure, but it won’t be used if you include it so it doesn’t hurt to leave it in. A function application on Azure is an archive generated by the Maven plugin. The function lives in the JAR file generated by this project. The sample creates it as an executable jar, using the thin layout, so that Azure can find the handler classes. If you prefer you can just use a regular flat JAR file. The dependencies should not be included.

    135.2.2 Build

    ./mvnw -U clean package

    135.2.3 Running the sample

    You can run the sample locally, just like the other Spring Cloud Function samples:

    and curl -H "Content-Type: text/plain" localhost:8080/function -d '{"value": "hello foobar"}'.

    You will need the az CLI app (see https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-java-maven for more detail). To deploy the function on Azure runtime:

    $ az login
    +$ mvn azure-functions:deploy

    On another terminal try this: curl https://<azure-function-url-from-the-log>/api/uppercase -d '{"value": "hello foobar!"}'. Please ensure that you use the right URL for the function above. Alternatively you can test the function in the Azure Dashboard UI (click on the function name, go to the right hand side and click "Test" and to the bottom right, "Run").

    The input type for the function in the Azure sample is a Foo with a single property called "value". So you need this to test it with something like below:

    {
    +  "value": "foobar"
    +}
    [Note]Note

    The Azure sample app is written in the "non-functional" style (using @Bean). The functional style (with just Function or ApplicationContextInitializer) is much faster on startup in Azure than the traditional @Bean style, so if you don’t need @Beans (or @EnableAutoConfiguration) it’s a good choice. Warm starts are not affected.

    135.3 Apache Openwhisk

    The OpenWhisk adapter is in the form of an executable jar that can be used in a a docker image to be deployed to Openwhisk. The platform works in request-response mode, listening on port 8080 on a specific endpoint, so the adapter is a simple Spring MVC application.

    135.3.1 Quick Start

    Implement a POF (be sure to use the functions package):

    package functions;
    +
    +import java.util.function.Function;
    +
    +public class Uppercase implements Function<String, String> {
    +
    +	public String apply(String input) {
    +		return input.toUpperCase();
    +	}
    +}

    Install it into your local Maven repository:

    ./mvnw clean install

    Create a function.properties file that provides its Maven coordinates. For example:

    dependencies.function: com.example:pof:0.0.1-SNAPSHOT

    Copy the openwhisk runner JAR to the working directory (same directory as the properties file):

    cp spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk/target/spring-cloud-function-adapter-openwhisk-2.0.0.BUILD-SNAPSHOT.jar runner.jar

    Generate a m2 repo from the --thin.dryrun of the runner JAR with the above properties file:

    java -jar -Dthin.root=m2 runner.jar --thin.name=function --thin.dryrun

    Use the following Dockerfile:

    FROM openjdk:8-jdk-alpine
    +VOLUME /tmp
    +COPY m2 /m2
    +ADD runner.jar .
    +ADD function.properties .
    +ENV JAVA_OPTS=""
    +ENTRYPOINT [ "java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "runner.jar", "--thin.root=/m2", "--thin.name=function", "--function.name=uppercase"]
    +EXPOSE 8080
    [Note]Note

    you could use a Spring Cloud Function app, instead of just a jar with a POF in it, in which case you would have to change the way the app runs in the container so that it picks up the main class as a source file. For example, you could change the ENTRYPOINT above and add --spring.main.sources=com.example.SampleApplication.

    Build the Docker image:

    docker build -t [username/appname] .

    Push the Docker image:

    docker push [username/appname]

    Use the OpenWhisk CLI (e.g. after vagrant ssh) to create the action:

    wsk action create example --docker [username/appname]

    Invoke the action:

    wsk action invoke example --result --param payload foo
    +{
    +    "result": "FOO"
    +}

    Part XVII. Spring Cloud Kubernetes

    This reference guide covers how to use Spring Cloud Kubernetes.

    136. Why do you need Spring Cloud Kubernetes?

    Spring Cloud Kubernetes provide Spring Cloud common interface implementations that consume Kubernetes native services. +The main objective of the projects provided in this repository is to facilitate the integration of Spring Cloud and Spring Boot applications running inside Kubernetes.

    137. Starters

    Starters are convenient dependency descriptors you can include in your +application. Include a starter to get the dependencies and Spring Boot +auto-configuration for a feature set.

    StarterFeatures
    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes</artifactId>
    +</dependency>

    Discovery Client implementation that +resolves service names to Kubernetes Services.

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-config</artifactId>
    +</dependency>

    Load application properties from Kubernetes +ConfigMaps and Secrets. +Reload application properties when a ConfigMap or +Secret changes.

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
    +</dependency>

    Ribbon client-side load balancer with +server list obtained from Kubernetes Endpoints.

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-all</artifactId>
    +</dependency>

    All Spring Cloud Kubernetes features.

    138. DiscoveryClient for Kubernetes

    This project provides an implementation of Discovery Client +for Kubernetes. +This client lets you query Kubernetes endpoints (see services) by name. +A service is typically exposed by the Kubernetes API server as a collection of endpoints that represent http and https addresses and that a client can +access from a Spring Boot application running as a pod. This discovery feature is also used by the Spring Cloud Kubernetes Ribbon project +to fetch the list of the endpoints defined for an application to be load balanced.

    This is something that you get for free by adding the following dependency inside your project:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes</artifactId>
    +</dependency>

    To enable loading of the DiscoveryClient, add @EnableDiscoveryClient to the according configuration or application class, as the following example shows:

    @SpringBootApplication
    +@EnableDiscoveryClient
    +public class Application {
    +  public static void main(String[] args) {
    +    SpringApplication.run(Application.class, args);
    +  }
    +}

    Then you can inject the client in your code simply by autowiring it, as the following example shows:

    @Autowired
    +private DiscoveryClient discoveryClient;

    You can choose to enable DiscoveryClient from all namespaces by setting the following property in application.properties:

    spring.cloud.kubernetes.discovery.all-namespaces=true

    If, for any reason, you need to disable the DiscoveryClient, you can set the following property in application.properties:

    spring.cloud.kubernetes.discovery.enabled=false

    Some Spring Cloud components use the DiscoveryClient in order to obtain information about the local service instance. For +this to work, you need to align the Kubernetes service name with the spring.application.name property.

    [Note]Note

    spring.application.name has no effect as far as the name registered for the application within Kubernetes

    Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the +DiscoveryClient implementation accordingly. In order to enable this functionality you need to add +@EnableScheduling on a configuration class in your application.

    139. Kubernetes native service discovery

    Kubernetes itself is capable of (server side) service discovery (see: https://kubernetes.io/docs/concepts/services-networking/service/#discovering-services). +Using native kubernetes service discovery ensures compatibility with additional tooling, such as Istio (https://istio.io), a service mesh that is capable of load balancing, ribbon, circuit breaker, failover, and much more.

    The caller service then need only refer to names resolvable in a particular Kubernetes cluster. A simple implementation might use a spring RestTemplate that refers to a fully qualified domain name (FQDN), such as https://{service-name}.{namespace}.svc.{cluster}.local:{service-port}.

    Additionally, you can use Hystrix for:

    • Circuit breaker implementation on the caller side, by annotating the spring boot application class with @EnableCircuitBreaker
    • Fallback functionality, by annotating the respective method with @HystrixCommand(fallbackMethod=

    140. Kubernetes PropertySource implementations

    The most common approach to configuring your Spring Boot application is to create an application.properties or applicaiton.yaml or +an application-profile.properties or application-profile.yaml file that contains key-value pairs that provide customization values to your +application or Spring Boot starters. You can override these properties by specifying system properties or environment +variables.

    140.1 Using a ConfigMap PropertySource

    Kubernetes provides a resource named ConfigMap to externalize the +parameters to pass to your application in the form of key-value pairs or embedded application.properties or application.yaml files. +The Spring Cloud Kubernetes Config project makes Kubernetes ConfigMap instances available +during application bootstrapping and triggers hot reloading of beans or Spring context when changes are detected on +observed ConfigMap instances.

    The default behavior is to create a ConfigMapPropertySource based on a Kubernetes ConfigMap that has a metadata.name value of either the name of +your Spring application (as defined by its spring.application.name property) or a custom name defined within the +bootstrap.properties file under the following key: spring.cloud.kubernetes.config.name.

    However, more advanced configuration is possible where you can use multiple ConfigMap instances. +The spring.cloud.kubernetes.config.sources list makes this possible. +For example, you could define the following ConfigMap instances:

    spring:
    +  application:
    +    name: cloud-k8s-app
    +  cloud:
    +    kubernetes:
    +      config:
    +        name: default-name
    +        namespace: default-namespace
    +        sources:
    +         # Spring Cloud Kubernetes looks up a ConfigMap named c1 in namespace default-namespace
    +         - name: c1
    +         # Spring Cloud Kubernetes looks up a ConfigMap named default-name in whatever namespace n2
    +         - namespace: n2
    +         # Spring Cloud Kubernetes looks up a ConfigMap named c3 in namespace n3
    +         - namespace: n3
    +           name: c3

    In the preceding example, if spring.cloud.kubernetes.config.namespace had not been set, +the ConfigMap named c1 would be looked up in the namespace that the application runs.

    Any matching ConfigMap that is found is processed as follows:

    • Apply individual configuration properties.
    • Apply as yaml the content of any property named application.yaml.
    • Apply as a properties file the content of any property named application.properties.

    The single exception to the aforementioned flow is when the ConfigMap contains a single key that indicates +the file is a YAML or properties file. In that case, the name of the key does NOT have to be application.yaml or +application.properties (it can be anything) and the value of the property is treated correctly. +This features facilitates the use case where the ConfigMap was created by using something like the following:

    kubectl create configmap game-config --from-file=/path/to/app-config.yaml

    Assume that we have a Spring Boot application named demo that uses the following properties to read its thread pool +configuration.

    • pool.size.core
    • pool.size.maximum

    This can be externalized to config map in yaml format as follows:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  pool.size.core: 1
    +  pool.size.max: 16

    Individual properties work fine for most cases. However, sometimes, embedded yaml is more convenient. In this case, we +use a single property named application.yaml to embed our yaml, as follows:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  application.yaml: |-
    +    pool:
    +      size:
    +        core: 1
    +        max:16

    The following example also works:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  custom-name.yaml: |-
    +    pool:
    +      size:
    +        core: 1
    +        max:16

    You can also configure Spring Boot applications differently depending on active profiles that are merged together +when the ConfigMap is read. You can provide different property values for different profiles by using an +application.properties or application.yaml property, specifying profile-specific values, each in their own document +(indicated by the --- sequence), as follows:

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  application.yml: |-
    +    greeting:
    +      message: Say Hello to the World
    +    farewell:
    +      message: Say Goodbye
    +    ---
    +    spring:
    +      profiles: development
    +    greeting:
    +      message: Say Hello to the Developers
    +    farewell:
    +      message: Say Goodbye to the Developers
    +    ---
    +    spring:
    +      profiles: production
    +    greeting:
    +      message: Say Hello to the Ops

    In the preceding case, the configuration loaded into your Spring Application with the development profile is as follows:

      greeting:
    +    message: Say Hello to the Developers
    +  farewell:
    +    message: Say Goodbye to the Developers

    However, if the production profile is active, the configuration becomes:

      greeting:
    +    message: Say Hello to the Ops
    +  farewell:
    +    message: Say Goodbye

    If both profiles are active, the property that appears last within the ConfigMap overwrites any preceding values.

    Another option is to create a different config map per profile and spring boot will automatically fetch it based +on active profiles

    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo
    +data:
    +  application.yml: |-
    +    greeting:
    +      message: Say Hello to the World
    +    farewell:
    +      message: Say Goodbye
    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo-development
    +data:
    +  application.yml: |-
    +    spring:
    +      profiles: development
    +    greeting:
    +      message: Say Hello to the Developers
    +    farewell:
    +      message: Say Goodbye to the Developers
    kind: ConfigMap
    +apiVersion: v1
    +metadata:
    +  name: demo-production
    +data:
    +  application.yml: |-
    +    spring:
    +      profiles: production
    +    greeting:
    +      message: Say Hello to the Ops
    +    farewell:
    +      message: Say Goodbye

    To tell Spring Boot which profile should be enabled at bootstrap, you can pass a system property to the Java +command. To do so, you can launch your Spring Boot application with an environment variable that you can define with the OpenShift +DeploymentConfig or Kubernetes ReplicationConfig resource file, as follows:

    apiVersion: v1
    +kind: DeploymentConfig
    +spec:
    +  replicas: 1
    +  ...
    +    spec:
    +      containers:
    +      - env:
    +        - name: JAVA_APP_DIR
    +          value: /deployments
    +        - name: JAVA_OPTIONS
    +          value: -Dspring.profiles.active=developer
    [Note]Note

    You should check the security configuration section. To access config maps from inside a pod you need to have the correct +Kubernetes service accounts, roles and role bindings.

    Another option for using ConfigMap instances is to mount them into the Pod by running the Spring Cloud Kubernetes application +and having Spring Cloud Kubernetes read them from the file system. +This behavior is controlled by the spring.cloud.kubernetes.config.paths property. You can use it in +addition to or instead of the mechanism described earlier. +You can specify multiple (exact) file paths in spring.cloud.kubernetes.config.paths by using the , delimiter.

    [Note]Note

    You have to provide the full exact path to each property file, because directories are not being recursively parsed.

    Table 140.1. Properties:

    NameTypeDefaultDescription

    spring.cloud.kubernetes.config.enableApi

    Boolean

    true

    Enable or disable consuming ConfigMap instances through APIs

    spring.cloud.kubernetes.config.enabled

    Boolean

    true

    Enable ConfigMaps PropertySource

    spring.cloud.kubernetes.config.name

    String

    ${spring.application.name}

    Sets the name of ConfigMap to look up

    spring.cloud.kubernetes.config.namespace

    String

    Client namespace

    Sets the Kubernetes namespace where to lookup

    spring.cloud.kubernetes.config.paths

    List

    null

    Sets the paths where ConfigMap instances are mounted


    140.2 Secrets PropertySource

    Kubernetes has the notion of Secrets for storing +sensitive data such as passwords, OAuth tokens, and so on. This project provides integration with Secrets to make secrets +accessible by Spring Boot applications. You can explicitly enable or disable This feature by setting the spring.cloud.kubernetes.secrets.enabled property.

    When enabled, the SecretsPropertySource looks up Kubernetes for Secrets from the following sources:

    1. Reading recursively from secrets mounts
    2. Named after the application (as defined by spring.application.name)
    3. Matching some labels

    Note:

    By default, consuming Secrets through the API (points 2 and 3 above) is not enabled for security reasons. The permission 'list' on secrets allows clients to inspect secrets values in the specified namespace. +Further, we recommend that containers share secrets through mounted volumes.

    If you enable consuming Secrets through the API, we recommend that you limit access to Secrets by using an authorization policy, such as RBAC. +For more information about risks and best practices when consuming Secrets through the API refer to this doc.

    If the secrets are found, their data is made available to the application.

    Assume that we have a spring boot application named demo that uses properties to read its database +configuration. We can create a Kubernetes secret by using the following command:

    oc create secret generic db-secret --from-literal=username=user --from-literal=password=p455w0rd

    The preceding command would create the following secret (which you can see by using oc get secrets db-secret -o yaml):

    apiVersion: v1
    +data:
    +  password: cDQ1NXcwcmQ=
    +  username: dXNlcg==
    +kind: Secret
    +metadata:
    +  creationTimestamp: 2017-07-04T09:15:57Z
    +  name: db-secret
    +  namespace: default
    +  resourceVersion: "357496"
    +  selfLink: /api/v1/namespaces/default/secrets/db-secret
    +  uid: 63c89263-6099-11e7-b3da-76d6186905a8
    +type: Opaque

    Note that the data contains Base64-encoded versions of the literal provided by the create command.

    Your application can then use this secret — for example, by exporting the secret’s value as environment variables:

    apiVersion: v1
    +kind: Deployment
    +metadata:
    +  name: ${project.artifactId}
    +spec:
    +   template:
    +     spec:
    +       containers:
    +         - env:
    +            - name: DB_USERNAME
    +              valueFrom:
    +                 secretKeyRef:
    +                   name: db-secret
    +                   key: username
    +            - name: DB_PASSWORD
    +              valueFrom:
    +                 secretKeyRef:
    +                   name: db-secret
    +                   key: password

    You can select the Secrets to consume in a number of ways:

    1. By listing the directories where secrets are mapped:

      -Dspring.cloud.kubernetes.secrets.paths=/etc/secrets/db-secret,etc/secrets/postgresql

      If you have all the secrets mapped to a common root, you can set them like:

      -Dspring.cloud.kubernetes.secrets.paths=/etc/secrets
    2. By setting a named secret:

      -Dspring.cloud.kubernetes.secrets.name=db-secret
    3. By defining a list of labels:

      -Dspring.cloud.kubernetes.secrets.labels.broker=activemq
      +-Dspring.cloud.kubernetes.secrets.labels.db=postgresql

    Table 140.2. Properties:

    NameTypeDefaultDescription

    spring.cloud.kubernetes.secrets.enableApi

    Boolean

    false

    Enables or disables consuming secrets through APIs (examples 2 and 3)

    spring.cloud.kubernetes.secrets.enabled

    Boolean

    true

    Enable Secrets PropertySource

    spring.cloud.kubernetes.secrets.name

    String

    ${spring.application.name}

    Sets the name of the secret to look up

    spring.cloud.kubernetes.secrets.namespace

    String

    Client namespace

    Sets the Kubernetes namespace where to look up

    spring.cloud.kubernetes.secrets.labels

    Map

    null

    Sets the labels used to lookup secrets

    spring.cloud.kubernetes.secrets.paths

    List

    null

    Sets the paths where secrets are mounted (example 1)


    Notes:

    • The spring.cloud.kubernetes.secrets.labels property behaves as defined by +Map-based binding.
    • The spring.cloud.kubernetes.secrets.paths property behaves as defined by +Collection-based binding.
    • Access to secrets through the API may be restricted for security reasons. The preferred way is to mount secrets to the Pod.

    You can find an example of an application that uses secrets (though it has not been updated to use the new spring-cloud-kubernetes project) at +spring-boot-camel-config

    140.3 PropertySource Reload

    Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. +The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related ConfigMap or +Secret changes.

    By default, this feature is disabled. You can enable it by using the spring.cloud.kubernetes.reload.enabled=true configuration property (for example, in the application.properties file).

    The following levels of reload are supported (by setting the spring.cloud.kubernetes.reload.strategy property): +* refresh (default): Only configuration beans annotated with @ConfigurationProperties or @RefreshScope are reloaded. +This reload level leverages the refresh feature of Spring Cloud Context. +* restart_context: the whole Spring ApplicationContext is gracefully restarted. Beans are recreated with the new configuration. +* shutdown: the Spring ApplicationContext is shut down to activate a restart of the container. + When you use this level, make sure that the lifecycle of all non-daemon threads is bound to the ApplicationContext +and that a replication controller or replica set is configured to restart the pod.

    Assuming that the reload feature is enabled with default settings (refresh mode), the following bean is refreshed when the config map changes:

    @Configuration
    +@ConfigurationProperties(prefix = "bean")
    +public class MyConfig {
    +
    +    private String message = "a message that can be changed live";
    +
    +    // getter and setters
    +
    +}

    To see that changes effectively happen, you can create another bean that prints the message periodically, as follows

    @Component
    +public class MyBean {
    +
    +    @Autowired
    +    private MyConfig config;
    +
    +    @Scheduled(fixedDelay = 5000)
    +    public void hello() {
    +        System.out.println("The message is: " + config.getMessage());
    +    }
    +}

    You can change the message printed by the application by using a ConfigMap, as follows:

    apiVersion: v1
    +kind: ConfigMap
    +metadata:
    +  name: reload-example
    +data:
    +  application.properties: |-
    +    bean.message=Hello World!

    Any change to the property named bean.message in the ConfigMap associated with the pod is reflected in the +output. More generally speaking, changes associated to properties prefixed with the value defined by the prefix +field of the @ConfigurationProperties annotation are detected and reflected in the application. +Associating a ConfigMap with a pod is explained earlier in this chapter.

    The full example is available in spring-cloud-kubernetes-reload-example.

    The reload feature supports two operating modes: +* Event (default): Watches for changes in config maps or secrets by using the Kubernetes API (web socket). +Any event produces a re-check on the configuration and, in case of changes, a reload. +The view role on the service account is required in order to listen for config map changes. A higher level role (such as edit) is required for secrets +(by default, secrets are not monitored). +* Polling: Oeriodically re-creates the configuration from config maps and secrets to see if it has changed. +You can configure the polling period by using the spring.cloud.kubernetes.reload.period property and defaults to 15 seconds. +It requires the same role as the monitored property source. +This means, for example, that using polling on file-mounted secret sources does not require particular privileges.

    Table 140.3. Properties:

    NameTypeDefaultDescription

    spring.cloud.kubernetes.reload.period

    Duration

    15s

    The period for verifying changes when using the polling strategy

    spring.cloud.kubernetes.reload.enabled

    Boolean

    false

    Enables monitoring of property sources and configuration reload

    spring.cloud.kubernetes.reload.monitoring-config-maps

    Boolean

    true

    Allow monitoring changes in config maps

    spring.cloud.kubernetes.reload.monitoring-secrets

    Boolean

    false

    Allow monitoring changes in secrets

    `spring.cloud.kubernetes.reload.strategy `

    Enum

    refresh

    The strategy to use when firing a reload (refresh, restart_context, or shutdown)

    spring.cloud.kubernetes.reload.mode

    Enum

    event

    Specifies how to listen for changes in property sources (event or polling)


    Notes: +* You should not use properties under spring.cloud.kubernetes.reload in config maps or secrets. Changing such properties at runtime may lead to unexpected results. +* Deleting a property or the whole config map does not restore the original state of the beans when you use the refresh level.

    141. Ribbon Discovery in Kubernetes

    Spring Cloud client applications that call a microservice should be interested on relying on a client load-balancing +feature in order to automatically discover at which endpoint(s) it can reach a given service. This mechanism has been +implemented within the spring-cloud-kubernetes-ribbon project, where a +Kubernetes client populates a Ribbon ServerList that contains information +about such endpoints.

    The implementation is part of the following starter that you can use by adding its dependency to your pom file:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
    +    <version>${latest.version}</version>
    +</dependency>

    When the list of the endpoints is populated, the Kubernetes client searches the registered endpoints that live in +the current namespace or project by matching the service name defined in the Ribbon Client annotation, as follows:

    @RibbonClient(name = "name-service")

    You can configure Ribbon’s behavior by providing properties in your application.properties (through your application’s +dedicated ConfigMap) by using the following format: <name of your service>.ribbon.<Ribbon configuration key>, where:

    • <name of your service> corresponds to the service name you access over Ribbon, as configured by using the +@RibbonClient annotation (such as name-service in the preceding example).
    • <Ribbon configuration key> is one of the Ribbon configuration keys defined by +Ribbon’s CommonClientConfigKey class.

    Additionally, the spring-cloud-kubernetes-ribbon project defines two additional configuration keys to further +control how Ribbon interacts with Kubernetes. In particular, if an endpoint defines multiple ports, the default +behavior is to use the first one found. To select more specifically which port to use in a multi-port service, you can use +the PortName key. If you want to specify in which Kubernetes namespace the target service should be looked up, you can use +the KubernetesNamespace key, remembering in both instances to prefix these keys with your service name and +ribbon prefix, as specified earlier.

    The following examples use this module for ribbon discovery:

    [Note]Note

    You can disable the Ribbon discovery client by setting the spring.cloud.kubernetes.ribbon.enabled=false key within the application properties file.

    142. Kubernetes Ecosystem Awareness

    All of the features described earlier in this guide work equally well, regardless of whether your application is running inside +Kubernetes. This is really helpful for development and troubleshooting. +From a development point of view, this lets you start your Spring Boot application and debug one +of the modules that is part of this project. You need not deploy it in Kubernetes, +as the code of the project relies on the +Fabric8 Kubernetes Java client, which is a fluent DSL that can +communicate by using http protocol to the REST API of the Kubernetes Server.

    To disable the integration with Kubernetes you can set spring.cloud.kubernetes.enabled to false. Please be aware that when spring-cloud-kubernetes-config is on the classpath, +spring.cloud.kubernetes.enabled should be set in bootstrap.{properties|yml} (or the profile specific one) otherwise it should be in application.{properties|yml} (or the profile specific one). +Also note that these properties: spring.cloud.kubernetes.config.enabled and spring.cloud.kubernetes.secrets.enabled only take effect when set in bootstrap.{properties|yml}

    142.1 Kubernetes Profile Autoconfiguration

    When the application runs as a pod inside Kubernetes, a Spring profile named kubernetes automatically gets activated. +This lets you customize the configuration, to define beans that are applied when the Spring Boot application is deployed +within the Kubernetes platform (for example, different development and production configuration).

    142.2 Istio Awareness

    When you include the spring-cloud-kubernetes-istio module in the application classpath, a new profile is added to the application, +provided the application is running inside a Kubernetes Cluster with Istio installed. You can then use +spring @Profile("istio") annotations in your Beans and @Configuration classes.

    The Istio awareness module uses me.snowdrop:istio-client to interact with Istio APIs, letting us discover traffic rules, circuit breakers, and so on, +making it easy for our Spring Boot applications to consume this data to dynamically configure themselves according to the environment.

    143. Pod Health Indicator

    Spring Boot uses HealthIndicator to expose info about the health of an application. +That makes it really useful for exposing health-related information to the user and makes it a good fit for use as readiness probes.

    The Kubernetes health indicator (which is part of the core module) exposes the following info:

    • Pod name, IP address, namespace, service account, node name, and its IP address
    • A flag that indicates whether the Spring Boot application is internal or external to Kubernetes

    144. Leader Election

    <TBD>

    145. Security Configurations Inside Kubernetes

    145.1 Namespace

    Most of the components provided in this project need to know the namespace. For Kubernetes (1.3+), the namespace is made available to the pod as part of the service account secret and is automatically detected by the client. +For earlier versions, it needs to be specified as an environment variable to the pod. A quick way to do this is as follows:

          env:
    +      - name: "KUBERNETES_NAMESPACE"
    +        valueFrom:
    +          fieldRef:
    +            fieldPath: "metadata.namespace"

    145.2 Service Account

    For distributions of Kubernetes that support more fine-grained role-based access within the cluster, you need to make sure a pod that runs with spring-cloud-kubernetes has access to the Kubernetes API. +For any service accounts you assign to a deployment or pod, you need to make sure they have the correct roles. For example, you can add cluster-reader permissions to your default service account, depending on the project you’re in.

    146. Service Registry Implementation

    In Kubernetes service registration is controlled by the platform, the application itself does not control +registration as it may do in other platforms. For this reason using spring.cloud.service-registry.auto-registration.enabled +or setting @EnableDiscoveryClient(autoRegister=false) will have no effect in Spring Cloud Kubernetes.

    147. Examples

    Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by +following the Spring Cloud interfaces.

    In your applications, you need to add the spring-cloud-kubernetes-discovery dependency to your classpath and remove any other dependency that contains a DiscoveryClient implementation (that is, a Eureka discovery client). +The same applies for PropertySourceLocator, where you need to add to the classpath the spring-cloud-kubernetes-config and remove any other dependency that contains a PropertySourceLocator implementation (that is, a configuration server client).

    The following projects highlight the usage of these dependencies and demonstrate how you can use these libraries from any Spring Boot application:

    148. Other Resources

    This section lists other resources, such as presentations (slides) and videos about Spring Cloud Kubernetes.

    Please feel free to submit other resources through pull requests to this repository.

    149. Building

    149.1 Basic Compile and Test

    To build the source you will need to install JDK 1.7.

    Spring Cloud uses Maven for most build-related activities, and you +should be able to get off the ground quite quickly by cloning the +project you are interested in and typing

    $ ./mvnw install
    [Note]Note

    You can also install Maven (>=3.3.3) yourself and run the mvn command +in place of ./mvnw in the examples below. If you do that you also +might need to add -P spring if your local Maven settings do not +contain repository declarations for spring pre-release artifacts.

    [Note]Note

    Be aware that you might need to increase the amount of memory +available to Maven by setting a MAVEN_OPTS environment variable with +a value like -Xmx512m -XX:MaxPermSize=128m. We try to cover this in +the .mvn configuration, so if you find you have to do it to make a +build succeed, please raise a ticket to get the settings added to +source control.

    For hints on how to build the project look in .travis.yml if there +is one. There should be a "script" and maybe "install" command. Also +look at the "services" section to see if any services need to be +running locally (e.g. mongo or rabbit). Ignore the git-related bits +that you might find in "before_install" since they’re related to setting git +credentials and you already have those.

    The projects that require middleware generally include a +docker-compose.yml, so consider using +Docker Compose to run the middeware servers +in Docker containers. See the README in the +scripts demo +repository for specific instructions about the common cases of mongo, +rabbit and redis.

    [Note]Note

    If all else fails, build with the command from .travis.yml (usually +./mvnw install).

    149.2 Documentation

    The spring-cloud-build module has a "docs" profile, and if you switch +that on it will try to build asciidoc sources from +src/main/asciidoc. As part of that process it will look for a +README.adoc and process it by loading all the includes, but not +parsing or rendering it, just copying it to ${main.basedir} +(defaults to $../../../.., i.e. the root of the project). If there are +any changes in the README it will then show up after a Maven build as +a modified file in the correct place. Just commit it and push the change.

    149.3 Working with the code

    If you don’t have an IDE preference we would recommend that you use +Spring Tools Suite or +Eclipse when working with the code. We use the +m2eclipse eclipse plugin for maven support. Other IDEs and tools +should also work without issue as long as they use Maven 3.3.3 or better.

    149.3.1 Importing into eclipse with m2eclipse

    We recommend the m2eclipse eclipse plugin when working with +eclipse. If you don’t already have m2eclipse installed it is available from the "eclipse +marketplace".

    [Note]Note

    Older versions of m2e do not support Maven 3.3, so once the +projects are imported into Eclipse you will also need to tell +m2eclipse to use the right profile for the projects. If you +see many different errors related to the POMs in the projects, check +that you have an up to date installation. If you can’t upgrade m2e, +add the "spring" profile to your settings.xml. Alternatively you can +copy the repository settings from the "spring" profile of the parent +pom into your settings.xml.

    149.3.2 Importing into eclipse without m2eclipse

    If you prefer not to use m2eclipse you can generate eclipse project metadata using the +following command:

    $ ./mvnw eclipse:eclipse

    The generated eclipse projects can be imported by selecting import existing projects +from the file menu.

    150. Contributing

    Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below.

    150.1 Sign the Contributor License Agreement

    Before we accept a non-trivial patch or pull request we will need you to sign the +Contributor License Agreement. +Signing the contributor’s agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests.

    150.2 Code of Conduct

    This project adheres to the Contributor Covenant code of +conduct. By participating, you are expected to uphold this code. Please report +unacceptable behavior to spring-code-of-conduct@pivotal.io.

    150.3 Code Conventions and Housekeeping

    None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge.

    • Use the Spring Framework code format conventions. If you use Eclipse +you can import formatter settings using the +eclipse-code-formatter.xml file from the +Spring +Cloud Build project. If using IntelliJ, you can use the +Eclipse Code Formatter +Plugin to import the same file.
    • Make sure all new .java files to have a simple Javadoc class comment with at least an +@author tag identifying you, and preferably at least a paragraph on what the class is +for.
    • Add the ASF license header comment to all new .java files (copy from existing files +in the project)
    • Add yourself as an @author to the .java files that you modify substantially (more +than cosmetic changes).
    • Add some Javadocs and, if you change the namespace, some XSD doc elements.
    • A few unit tests would help a lot as well — someone has to do it.
    • If no-one else is using your branch, please rebase it against the current master (or +other target branch in the main project).
    • When writing a commit message please follow these conventions, +if you are fixing an existing issue please add Fixes gh-XXXX at the end of the commit +message (where XXXX is the issue number).

    150.4 Checkstyle

    Spring Cloud Build comes with a set of checkstyle rules. You can find them in the spring-cloud-build-tools module. The most notable files under the module are:

    spring-cloud-build-tools/.  +

    └── src
    +    ├── checkstyle
    +    │   └── checkstyle-suppressions.xml 1
    +    └── main
    +        └── resources
    +            ├── checkstyle-header.txt 2
    +            └── checkstyle.xml 3

    +

    3

    Default Checkstyle rules

    2

    File header setup

    1

    Default suppression rules

    150.4.1 Checkstyle configuration

    Checkstyle rules are disabled by default. To add checkstyle to your project just define the following properties and plugins.

    pom.xml.  +

    <properties>
    +<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError> 1
    +        <maven-checkstyle-plugin.failsOnViolation>true
    +        </maven-checkstyle-plugin.failsOnViolation> 2
    +        <maven-checkstyle-plugin.includeTestSourceDirectory>true
    +        </maven-checkstyle-plugin.includeTestSourceDirectory> 3
    +</properties>
    +
    +<build>
    +        <plugins>
    +            <plugin> 4
    +                <groupId>io.spring.javaformat</groupId>
    +                <artifactId>spring-javaformat-maven-plugin</artifactId>
    +            </plugin>
    +            <plugin> 5
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-checkstyle-plugin</artifactId>
    +            </plugin>
    +        </plugins>
    +
    +    <reporting>
    +        <plugins>
    +            <plugin> 6
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-checkstyle-plugin</artifactId>
    +            </plugin>
    +        </plugins>
    +    </reporting>
    +</build>

    +

    1

    Fails the build upon Checkstyle errors

    2

    Fails the build upon Checkstyle violations

    3

    Checkstyle analyzes also the test sources

    4

    Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules

    5 6

    Add checkstyle plugin to your build and reporting phases

    If you need to suppress some rules (e.g. line length needs to be longer), then it’s enough for you to define a file under ${project.root}/src/checkstyle/checkstyle-suppressions.xml with your suppressions. Example:

    projectRoot/src/checkstyle/checkstyle-suppresions.xml.  +

    <?xml version="1.0"?>
    +<!DOCTYPE suppressions PUBLIC
    +		"-//Puppy Crawl//DTD Suppressions 1.1//EN"
    +		"https://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
    +<suppressions>
    +	<suppress files=".*ConfigServerApplication\.java" checks="HideUtilityClassConstructor"/>
    +	<suppress files=".*ConfigClientWatch\.java" checks="LineLengthCheck"/>
    +</suppressions>

    +

    It’s advisable to copy the ${spring-cloud-build.rootFolder}/.editorconfig and ${spring-cloud-build.rootFolder}/.springformat to your project. That way, some default formatting rules will be applied. You can do so by running this script:

    $ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig
    +$ touch .springformat

    150.5 IDE setup

    150.5.1 Intellij IDEA

    In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. +The following files can be found in the Spring Cloud Build project.

    spring-cloud-build-tools/.  +

    └── src
    +    ├── checkstyle
    +    │   └── checkstyle-suppressions.xml 1
    +    └── main
    +        └── resources
    +            ├── checkstyle-header.txt 2
    +            ├── checkstyle.xml 3
    +            └── intellij
    +                ├── Intellij_Project_Defaults.xml 4
    +                └── Intellij_Spring_Boot_Java_Conventions.xml 5

    +

    3

    Default Checkstyle rules

    2

    File header setup

    1

    Default suppression rules

    4

    Project defaults for Intellij that apply most of Checkstyle rules

    5

    Project style conventions for Intellij that apply most of Checkstyle rules

    Figure 150.1. Code style

    Code style

    Go to FileSettingsEditorCode style. There click on the icon next to the Scheme section. There, click on the Import Scheme value and pick the Intellij IDEA code style XML option. Import the spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml file.

    Figure 150.2. Inspection profiles

    Code style

    Go to FileSettingsEditorInspections. There click on the icon next to the Profile section. There, click on the Import Profile and import the spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml file.

    Checkstyle. To have Intellij work with Checkstyle, you have to install the Checkstyle plugin. It’s advisable to also install the Assertions2Assertj to automatically convert the JUnit assertions

    Checkstyle

    Go to FileSettingsOther settingsCheckstyle. There click on the + icon in the Configuration file section. There, you’ll have to define where the checkstyle rules should be picked from. In the image above, we’ve picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build’s GitHub repository (e.g. for the checkstyle.xml : https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml). We need to provide the following variables:

    [Important]Important

    Remember to set the Scan Scope to All sources since we apply checkstyle rules for production and test sources.

    Part XVIII. Spring Cloud GCP

    João André Martins; Jisha Abubaker; Ray Tsang; Mike Eltsufin; Artem Bilan; Andreas Berger; Balint Pato; Chengyuan Zhao; Dmitry Solomakha; Elena Felder; Daniel Zou

    151. Introduction

    The Spring Cloud GCP project makes the Spring Framework a first-class citizen of Google Cloud Platform (GCP).

    Spring Cloud GCP lets you leverage the power and simplicity of the Spring Framework to:

    1. Analyze your images for text, objects, and other content with Google Cloud Vision
    2. Use Spring Security via Google Cloud IAP
    3. Map objects, relationships, and collections with Spring Data Cloud Spanner and Spring Data Cloud Datastore
    4. Publish and subscribe to Google Cloud Pub/Sub topics
    5. Configure Spring JDBC with a few properties to use Google Cloud SQL
    6. Write and read from Spring Resources backed up by Google Cloud Storage
    7. Exchange messages with Spring Integration using Google Cloud Pub/Sub on the background
    8. Trace the execution of your app with Spring Cloud Sleuth and Google Stackdriver Trace
    9. Configure your app with Spring Cloud Config, backed up by the Google Runtime Configuration API
    10. Consume and produce Google Cloud Storage data via Spring Integration GCS Channel Adapters

    152. Dependency Management

    The Spring Cloud GCP Bill of Materials (BOM) contains the versions of all the dependencies it uses.

    If you’re a Maven user, adding the following to your pom.xml file will allow you to not specify any Spring Cloud GCP dependency versions. +Instead, the version of the BOM you’re using determines the versions of the used dependencies.

    <dependencyManagement>
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.springframework.cloud</groupId>
    +            <artifactId>spring-cloud-gcp-dependencies</artifactId>
    +            <version>{project-version}</version>
    +            <type>pom</type>
    +            <scope>import</scope>
    +        </dependency>
    +    </dependencies>
    +</dependencyManagement>

    In the following sections, it will be assumed you are using the Spring Cloud GCP BOM and the dependency snippets will not contain versions.

    Gradle users can achieve the same kind of BOM experience using Spring’s dependency-management-plugin Gradle plugin. +For simplicity, the Gradle dependency snippets in the remainder of this document will also omit their versions.

    153. Getting started

    There are many available resources to get you up to speed with our libraries as quickly as possible.

    153.1 Spring Initializr

    There are three entries in Spring Initializr for Spring Cloud GCP.

    153.1.1 GCP Support

    The GCP Support entry contains auto-configuration support for every Spring Cloud GCP integration. +Most of the autoconfiguration code is only enabled if other dependencies are added to the classpath.

    Spring Cloud GCP StarterRequired dependencies

    Config

    org.springframework.cloud:spring-cloud-gcp-starter-config

    Cloud Spanner

    org.springframework.cloud:spring-cloud-gcp-starter-data-spanner

    Cloud Datastore

    org.springframework.cloud:spring-cloud-gcp-starter-data-datastore

    Logging

    org.springframework.cloud:spring-cloud-gcp-starter-logging

    SQL - MySql

    org.springframework.cloud:spring-cloud-gcp-starter-sql-mysql

    SQL - PostgreSQL

    org.springframework.cloud:spring-cloud-gcp-starter-sql-postgres

    Trace

    org.springframework.cloud:spring-cloud-gcp-starter-trace

    Vision

    org.springframework.cloud:spring-cloud-gcp-starter-vision

    Security - IAP

    org.springframework.cloud:spring-cloud-gcp-starter-security-iap

    153.1.2 GCP Messaging

    The GCP Messaging entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Pub/Sub integrations work out of the box.

    153.1.3 GCP Storage

    The GCP Storage entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Storage integrations work out of the box.

    153.2 Code Samples

    There are code samples available that demonstrate the usage of all our integrations.

    For example, the Vision API sample shows how to use spring-cloud-gcp-starter-vision to automatically configure Vision API clients.

    153.3 Code Challenges

    In a code challenge, you perform a task step by step, using one integration. +There are a number of challenges available in the Google Developers Codelabs page.

    153.4 Getting Started Guides

    A Spring Getting Started guide on messaging with Spring Integration Channel Adapters for Google Cloud Pub/Sub is available from Spring Guides.

    154. Spring Cloud GCP Core

    Each Spring Cloud GCP module uses GcpProjectIdProvider and CredentialsProvider to get the GCP project ID and access credentials.

    Spring Cloud GCP provides a Spring Boot starter to auto-configure the core components.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter'
    +}

    154.1 Project ID

    GcpProjectIdProvider is a functional interface that returns a GCP project ID string.

    public interface GcpProjectIdProvider {
    +	String getProjectId();
    +}

    The Spring Cloud GCP starter auto-configures a GcpProjectIdProvider. +If a spring.cloud.gcp.project-id property is specified, the provided GcpProjectIdProvider returns that property value.

    spring.cloud.gcp.project-id=my-gcp-project-id

    Otherwise, the project ID is discovered based on an +ordered list of rules:

    1. The project ID specified by the GOOGLE_CLOUD_PROJECT environment variable
    2. The Google App Engine project ID
    3. The project ID specified in the JSON credentials file pointed by the GOOGLE_APPLICATION_CREDENTIALS environment variable
    4. The Google Cloud SDK project ID
    5. The Google Compute Engine project ID, from the Google Compute Engine Metadata Server

    154.2 Credentials

    CredentialsProvider is a functional interface that returns the credentials to authenticate and authorize calls to Google Cloud Client Libraries.

    public interface CredentialsProvider {
    +  Credentials getCredentials() throws IOException;
    +}

    The Spring Cloud GCP starter auto-configures a CredentialsProvider. +It uses the spring.cloud.gcp.credentials.location property to locate the OAuth2 private key of a Google service account. +Keep in mind this property is a Spring Resource, so the credentials file can be obtained from a number of different locations such as the file system, classpath, URL, etc. +The next example specifies the credentials location property in the file system.

    spring.cloud.gcp.credentials.location=file:/usr/local/key.json

    Alternatively, you can set the credentials by directly specifying the spring.cloud.gcp.credentials.encoded-key property. +The value should be the base64-encoded account private key in JSON format.

    If that credentials aren’t specified through properties, the starter tries to discover credentials from a number of places:

    1. Credentials file pointed to by the GOOGLE_APPLICATION_CREDENTIALS environment variable
    2. Credentials provided by the Google Cloud SDK gcloud auth application-default login command
    3. Google App Engine built-in credentials
    4. Google Cloud Shell built-in credentials
    5. Google Compute Engine built-in credentials

    If your app is running on Google App Engine or Google Compute Engine, in most cases, you should omit the spring.cloud.gcp.credentials.location property and, instead, let the Spring Cloud GCP Starter get the correct credentials for those environments. +On App Engine Standard, the App Identity service account credentials are used, on App Engine Flexible, the Flexible service account credential are used and on Google Compute Engine, the Compute Engine Default Service Account is used.

    154.2.1 Scopes

    By default, the credentials provided by the Spring Cloud GCP Starter contain scopes for every service supported by Spring Cloud GCP.

    The Spring Cloud GCP starter allows you to configure a custom scope list for the provided credentials. +To do that, specify a comma-delimited list of Google OAuth2 scopes in the spring.cloud.gcp.credentials.scopes property.

    spring.cloud.gcp.credentials.scopes is a comma-delimited list of Google OAuth2 scopes for Google Cloud Platform services that the credentials returned by the provided CredentialsProvider support.

    spring.cloud.gcp.credentials.scopes=https://www.googleapis.com/auth/pubsub,https://www.googleapis.com/auth/sqlservice.admin

    You can also use DEFAULT_SCOPES placeholder as a scope to represent the starters default scopes, and append the additional scopes you need to add.

    spring.cloud.gcp.credentials.scopes=DEFAULT_SCOPES,https://www.googleapis.com/auth/cloud-vision

    154.3 Environment

    GcpEnvironmentProvider is a functional interface, auto-configured by the Spring Cloud GCP starter, that returns a GcpEnvironment enum. +The provider can help determine programmatically in which GCP environment (App Engine Flexible, App Engine Standard, Kubernetes Engine or Compute Engine) the application is deployed.

    public interface GcpEnvironmentProvider {
    +	GcpEnvironment getCurrentEnvironment();
    +}

    154.4 Spring Initializr

    This starter is available from Spring Initializr through the GCP Support entry.

    155. Google Cloud Pub/Sub

    Spring Cloud GCP provides an abstraction layer to publish to and subscribe from Google Cloud Pub/Sub topics and to create, list or delete Google Cloud Pub/Sub topics and subscriptions.

    A Spring Boot starter is provided to auto-configure the various required Pub/Sub components.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-pubsub'
    +}

    This starter is also available from Spring Initializr through the GCP Messaging entry.

    155.1 Pub/Sub Operations & Template

    PubSubOperations is an abstraction that allows Spring users to use Google Cloud Pub/Sub without depending on any Google Cloud Pub/Sub API semantics. +It provides the common set of operations needed to interact with Google Cloud Pub/Sub. +PubSubTemplate is the default implementation of PubSubOperations and it uses the Google Cloud Java Client for Pub/Sub to interact with Google Cloud Pub/Sub.

    PubSubTemplate depends on a PublisherFactory and a SubscriberFactory. +The PublisherFactory provides a Google Cloud Java Client for Pub/Sub Publisher. +The SubscriberFactory provides the Subscriber for asynchronous message pulling, as well as a SubscriberStub for synchronous pulling. +The Spring Boot starter for GCP Pub/Sub auto-configures a PublisherFactory and SubscriberFactory with default settings and uses the GcpProjectIdProvider and CredentialsProvider auto-configured by the Spring Boot GCP starter.

    The PublisherFactory implementation provided by Spring Cloud GCP Pub/Sub, DefaultPublisherFactory, caches Publisher instances by topic name, in order to optimize resource utilization.

    The PubSubOperations interface is actually a combination of PubSubPublisherOperations and PubSubSubscriberOperations with the corresponding PubSubPublisherTemplate and PubSubSubscriberTemplate implementations, which can be used individually or via the composite PubSubTemplate. +The rest of the documentation refers to PubSubTemplate, but the same applies to PubSubPublisherTemplate and PubSubSubscriberTemplate, depending on whether we’re talking about publishing or subscribing.

    155.1.1 Publishing to a topic

    PubSubTemplate provides asynchronous methods to publish messages to a Google Cloud Pub/Sub topic. +The publish() method takes in a topic name to post the message to, a payload of a generic type and, optionally, a map with the message headers.

    Here is an example of how to publish a message to a Google Cloud Pub/Sub topic:

    public void publishMessage() {
    +    this.pubSubTemplate.publish("topic", "your message payload", ImmutableMap.of("key1", "val1"));
    +}

    By default, the SimplePubSubMessageConverter is used to convert payloads of type byte[], ByteString, ByteBuffer, and String to Pub/Sub messages.

    JSON support

    For serialization and deserialization of POJOs using Jackson JSON, configure a JacksonPubSubMessageConverter bean, and the Spring Boot starter for GCP Pub/Sub will automatically wire it into the PubSubTemplate.

    // Note: The ObjectMapper is used to convert Java POJOs to and from JSON.
    +// You will have to configure your own instance if you are unable to depend
    +// on the ObjectMapper provided by Spring Boot starters.
    +@Bean
    +public JacksonPubSubMessageConverter jacksonPubSubMessageConverter(ObjectMapper objectMapper) {
    +    return new JacksonPubSubMessageConverter(objectMapper);
    +}

    Alternatively, you can set it directly by calling the setMessageConverter() method on the PubSubTemplate. +Other implementations of the PubSubMessageConverter can also be configured in the same manner.

    Please refer to our Pub/Sub JSON Payload Sample App as a reference for using this functionality.

    155.1.2 Subscribing to a subscription

    Google Cloud Pub/Sub allows many subscriptions to be associated to the same topic. +PubSubTemplate allows you to listen to subscriptions via the subscribe() method. +It relies on a SubscriberFactory object, whose only task is to generate Google Cloud Pub/Sub +Subscriber objects. +When listening to a subscription, messages will be pulled from Google Cloud Pub/Sub +asynchronously, at a certain interval.

    The Spring Boot starter for Google Cloud Pub/Sub auto-configures a SubscriberFactory.

    If Pub/Sub message payload conversion is desired, you can use the subscribeAndConvert() method, which will use the converter configured in the template.

    155.1.3 Pulling messages from a subscription

    Google Cloud Pub/Sub supports synchronous pulling of messages from a subscription. +This is different from subscribing to a subscription, in the sense that subscribing is an asynchronous task which polls the subscription on a set interval.

    The pullNext() method allows for a single message to be pulled and automatically acknowledged from a subscription. +The pull() method pulls a number of messages from a subscription, allowing for the retry settings to be configured. +Any messages received by pull() are not automatically acknowledged. +Instead, since they are of the kind AcknowledgeablePubsubMessage, you can acknowledge them by calling the ack() method, or negatively acknowledge them by calling the nack() method. +The pullAndAck() method does the same as the pull() method and, additionally, acknowledges all received messages.

    The pullAndConvert() method does the same as the pull() method and, additionally, converts the Pub/Sub binary payload to an object of the desired type, using the converter configured in the template.

    To acknowledge multiple messages received from pull() or pullAndConvert() at once, you can use the PubSubTemplate.ack() method. +You can also use the PubSubTemplate.nack() for negatively acknowledging messages.

    Using these methods for acknowledging messages in batches is more efficient than acknowledging messages individually, but they require the collection of messages to be from the same project.

    All ack(), nack(), and modifyAckDeadline() methods on messages as well as PubSubSubscriberTemplate are implemented asynchronously, returning a ListenableFuture<Void> to be able to process the asynchronous execution.

    PubSubTemplate uses a special subscriber generated by its SubscriberFactory to synchronously pull messages.

    155.2 Pub/Sub management

    PubSubAdmin is the abstraction provided by Spring Cloud GCP to manage Google Cloud Pub/Sub resources. +It allows for the creation, deletion and listing of topics and subscriptions.

    PubSubAdmin depends on GcpProjectIdProvider and either a CredentialsProvider or a TopicAdminClient and a SubscriptionAdminClient. +If given a CredentialsProvider, it creates a TopicAdminClient and a SubscriptionAdminClient with the Google Cloud Java Library for Pub/Sub default settings. +The Spring Boot starter for GCP Pub/Sub auto-configures a PubSubAdmin object using the GcpProjectIdProvider and the CredentialsProvider auto-configured by the Spring Boot GCP Core starter.

    155.2.1 Creating a topic

    PubSubAdmin implements a method to create topics:

    public Topic createTopic(String topicName)

    Here is an example of how to create a Google Cloud Pub/Sub topic:

    public void newTopic() {
    +    pubSubAdmin.createTopic("topicName");
    +}

    155.2.2 Deleting a topic

    PubSubAdmin implements a method to delete topics:

    public void deleteTopic(String topicName)

    Here is an example of how to delete a Google Cloud Pub/Sub topic:

    public void deleteTopic() {
    +    pubSubAdmin.deleteTopic("topicName");
    +}

    155.2.3 Listing topics

    PubSubAdmin implements a method to list topics:

    public List<Topic> listTopics

    Here is an example of how to list every Google Cloud Pub/Sub topic name in a project:

    public List<String> listTopics() {
    +    return pubSubAdmin
    +        .listTopics()
    +        .stream()
    +        .map(Topic::getNameAsTopicName)
    +        .map(TopicName::getTopic)
    +        .collect(Collectors.toList());
    +}

    155.2.4 Creating a subscription

    PubSubAdmin implements a method to create subscriptions to existing topics:

    public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline, String pushEndpoint)

    Here is an example of how to create a Google Cloud Pub/Sub subscription:

    public void newSubscription() {
    +    pubSubAdmin.createSubscription("subscriptionName", "topicName", 10, “https://my.endpoint/push”);
    +}

    Alternative methods with default settings are provided for ease of use. +The default value for ackDeadline is 10 seconds. +If pushEndpoint isn’t specified, the subscription uses message pulling, instead.

    public Subscription createSubscription(String subscriptionName, String topicName)
    public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline)
    public Subscription createSubscription(String subscriptionName, String topicName, String pushEndpoint)

    155.2.5 Deleting a subscription

    PubSubAdmin implements a method to delete subscriptions:

    public void deleteSubscription(String subscriptionName)

    Here is an example of how to delete a Google Cloud Pub/Sub subscription:

    public void deleteSubscription() {
    +    pubSubAdmin.deleteSubscription("subscriptionName");
    +}

    155.2.6 Listing subscriptions

    PubSubAdmin implements a method to list subscriptions:

    public List<Subscription> listSubscriptions()

    Here is an example of how to list every subscription name in a project:

    public List<String> listSubscriptions() {
    +    return pubSubAdmin
    +        .listSubscriptions()
    +        .stream()
    +        .map(Subscription::getNameAsSubscriptionName)
    +        .map(SubscriptionName::getSubscription)
    +        .collect(Collectors.toList());
    +}

    155.3 Configuration

    The Spring Boot starter for Google Cloud Pub/Sub provides the following configuration options:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.pubsub.enabled

    Enables or disables Pub/Sub auto-configuration

    No

    true

    spring.cloud.gcp.pubsub.subscriber.executor-threads

    Number of threads used by Subscriber instances created by SubscriberFactory

    No

    4

    spring.cloud.gcp.pubsub.publisher.executor-threads

    Number of threads used by Publisher instances created by PublisherFactory

    No

    4

    spring.cloud.gcp.pubsub.project-id

    GCP project ID where the Google Cloud Pub/Sub API is hosted, if different from the one in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.pubsub.credentials.location

    OAuth2 credentials for authenticating with the +Google Cloud Pub/Sub API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.pubsub.credentials.encoded-key

    Base64-encoded contents of OAuth2 account private key for authenticating with the +Google Cloud Pub/Sub API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.pubsub.credentials.scopes

    OAuth2 scope for Spring Cloud GCP +Pub/Sub credentials

    No

    https://www.googleapis.com/auth/pubsub

    spring.cloud.gcp.pubsub.subscriber.parallel-pull-count

    The number of pull workers

    No

    The available number of processors

    spring.cloud.gcp.pubsub.subscriber.max-ack-extension-period

    The maximum period a message ack deadline will be extended, in seconds

    No

    0

    spring.cloud.gcp.pubsub.subscriber.pull-endpoint

    The endpoint for synchronous pulling messages

    No

    pubsub.googleapis.com:443

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.total-timeout-seconds

    TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. +The higher the total timeout, the more retries can be attempted.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-retry-delay-second

    InitialRetryDelay controls the delay before the first retry. +Subsequent retries will use this value adjusted according to the RetryDelayMultiplier.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.retry-delay-multiplier

    RetryDelayMultiplier controls the change in retry delay. +The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call.

    No

    1

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-retry-delay-seconds

    MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier +can’t increase the retry delay higher than this amount.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-attempts

    MaxAttempts defines the maximum number of attempts to perform. +If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.jittered

    Jitter determines if the delay time should be randomized.

    No

    true

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-rpc-timeout-seconds

    InitialRpcTimeout controls the timeout for the initial RPC. +Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.rpc-timeout-multiplier

    RpcTimeoutMultiplier controls the change in RPC timeout. +The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call.

    No

    1

    spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-rpc-timeout-seconds

    MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier +can’t increase the RPC timeout higher than this amount.

    No

    0

    spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-element-count

    Maximum number of outstanding elements to keep in memory before enforcing flow control.

    No

    unlimited

    spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-request-bytes

    Maximum number of outstanding bytes to keep in memory before enforcing flow control.

    No

    unlimited

    spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.limit-exceeded-behavior

    The behavior when the specified limits are exceeded.

    No

    Block

    spring.cloud.gcp.pubsub.publisher.batching.element-count-threshold

    The element count threshold to use for batching.

    No

    unset (threshold does not apply)

    spring.cloud.gcp.pubsub.publisher.batching.request-byte-threshold

    The request byte threshold to use for batching.

    No

    unset (threshold does not apply)

    spring.cloud.gcp.pubsub.publisher.batching.delay-threshold-seconds

    The delay threshold to use for batching. +After this amount of time has elapsed (counting from the first element added), the elements will be wrapped up in a batch and sent.

    No

    unset (threshold does not apply)

    spring.cloud.gcp.pubsub.publisher.batching.enabled

    Enables batching.

    No

    false

    155.4 Sample

    A sample application is available.

    156. Spring Resources

    Spring Resources are an abstraction for a number of low-level resources, such as file system files, classpath files, servlet context-relative files, etc. +Spring Cloud GCP adds a new resource type: a Google Cloud Storage (GCS) object.

    A Spring Boot starter is provided to auto-configure the various Storage components.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-storage</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage'
    +}

    This starter is also available from Spring Initializr through the GCP Storage entry.

    156.1 Google Cloud Storage

    The Spring Resource Abstraction for Google Cloud Storage allows GCS objects to be accessed by their GCS URL using the @Value annotation:

    @Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]")
    +private Resource gcsResource;

    …​or the Spring application context

    SpringApplication.run(...).getResource("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]");

    This creates a Resource object that can be used to read the object, among other possible operations.

    It is also possible to write to a Resource, although a WriteableResource is required.

    @Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]")
    +private Resource gcsResource;
    +...
    +try (OutputStream os = ((WritableResource) gcsResource).getOutputStream()) {
    +  os.write("foo".getBytes());
    +}

    To work with the Resource as a Google Cloud Storage resource, cast it to GoogleStorageResource.

    If the resource path refers to an object on Google Cloud Storage (as opposed to a bucket), then the getBlob method can be called to obtain a Blob. +This type represents a GCS file, which has associated metadata, such as content-type, that can be set. +The createSignedUrl method can also be used to obtain signed URLs for GCS objects. +However, creating signed URLs requires that the resource was created using service account credentials.

    The Spring Boot Starter for Google Cloud Storage auto-configures the Storage bean required by the spring-cloud-gcp-storage module, based on the CredentialsProvider provided by the Spring Boot GCP starter.

    156.1.1 Setting the Content Type

    You can set the content-type of Google Cloud Storage files from their corresponding Resource objects:

    ((GoogleStorageResource)gcsResource).getBlob().toBuilder().setContentType("text/html").build().update();

    156.2 Configuration

    The Spring Boot Starter for Google Cloud Storage provides the following configuration options:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.storage.enabled

    Enables the GCP storage APIs.

    No

    true

    spring.cloud.gcp.storage.auto-create-files

    Creates files and buckets on Google Cloud Storage when writes are made to non-existent files

    No

    true

    spring.cloud.gcp.storage.credentials.location

    OAuth2 credentials for authenticating with the Google Cloud Storage API, if different from the ones in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.storage.credentials.encoded-key

    Base64-encoded contents of OAuth2 account private key for authenticating with the Google Cloud Storage API, if different from the ones in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.storage.credentials.scopes

    OAuth2 scope for Spring Cloud GCP Storage credentials

    No

    https://www.googleapis.com/auth/devstorage.read_write

    156.3 Sample

    A sample application and a codelab are available.

    157. Spring JDBC

    Spring Cloud GCP adds integrations with +Spring JDBC so you can run your MySQL or PostgreSQL databases in Google Cloud SQL using Spring JDBC, or other libraries that depend on it like Spring Data JPA.

    The Cloud SQL support is provided by Spring Cloud GCP in the form of two Spring Boot starters, one for MySQL and another one for PostgreSQL. +The role of the starters is to read configuration from properties and assume default settings so that user experience connecting to MySQL and PostgreSQL is as simple as possible.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-sql-postgresql</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-mysql'
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-postgresql'
    +}

    157.1 Prerequisites

    In order to use the Spring Boot Starters for Google Cloud SQL, the Google Cloud SQL API must be enabled in your GCP project.

    To do that, go to the API library page of the Google Cloud Console, search for "Cloud SQL API", click the first result and enable the API.

    [Note]Note

    There are several similar "Cloud SQL" results. +You must access the "Google Cloud SQL API" one and enable the API from there.

    157.2 Spring Boot Starter for Google Cloud SQL

    The Spring Boot Starters for Google Cloud SQL provide an auto-configured DataSource object. +Coupled with Spring JDBC, it provides a +JdbcTemplate object bean that allows for operations such as querying and modifying a database.

    public List<Map<String, Object>> listUsers() {
    +    return jdbcTemplate.queryForList("SELECT * FROM user;");
    +}

    You can rely on +Spring Boot data source auto-configuration to configure a DataSource bean. +In other words, properties like the SQL username, spring.datasource.username, and password, spring.datasource.password can be used. +There is also some configuration specific to Google Cloud SQL:

    Property name

    Description

    Default value

    spring.cloud.gcp.sql.enabled

    Enables or disables Cloud SQL auto configuration

    true

    spring.cloud.gcp.sql.database-name

    Name of the database to connect to.

     

    spring.cloud.gcp.sql.instance-connection-name

    A string containing a Google Cloud SQL instance’s project ID, region and name, each separated by a colon. +For example, my-project-id:my-region:my-instance-name.

     

    spring.cloud.gcp.sql.credentials.location

    File system path to the Google OAuth2 credentials private key file. +Used to authenticate and authorize new connections to a Google Cloud SQL instance.

    Default credentials provided by the Spring GCP Boot starter

    spring.cloud.gcp.sql.credentials.encoded-key

    Base64-encoded contents of OAuth2 account private key in JSON format. +Used to authenticate and authorize new connections to a Google Cloud SQL instance.

    Default credentials provided by the Spring GCP Boot starter

    [Note]Note

    If you provide your own spring.datasource.url, it will be ignored, unless you disable Cloud SQL auto configuration with spring.cloud.gcp.sql.enabled=false.

    157.2.1 DataSource creation flow

    Based on the previous properties, the Spring Boot starter for Google Cloud SQL creates a CloudSqlJdbcInfoProvider object which is used to obtain an instance’s JDBC URL and driver class name. +If you provide your own CloudSqlJdbcInfoProvider bean, it is used instead and the properties related to building the JDBC URL or driver class are ignored.

    The DataSourceProperties object provided by Spring Boot Autoconfigure is mutated in order to use the JDBC URL and driver class names provided by CloudSqlJdbcInfoProvider, unless those values were provided in the properties. +It is in the DataSourceProperties mutation step that the credentials factory is registered in a system property to be SqlCredentialFactory.

    DataSource creation is delegated to +Spring Boot. +You can select the type of connection pool (e.g., Tomcat, HikariCP, etc.) by adding their dependency to the classpath.

    Using the created DataSource in conjunction with Spring JDBC provides you with a fully configured and operational JdbcTemplate object that you can use to interact with your SQL database. +You can connect to your database with as little as a database and instance names.

    157.2.2 Troubleshooting tips

    Connection issues

    If you’re not able to connect to a database and see an endless loop of Connecting to Cloud SQL instance […​] on IP […​], it’s likely that exceptions are being thrown and logged at a level lower than your logger’s level. +This may be the case with HikariCP, if your logger is set to INFO or higher level.

    To see what’s going on in the background, you should add a logback.xml file to your application resources folder, that looks like this:

    <?xml version="1.0" encoding="UTF-8"?>
    +<configuration>
    +  <include resource="org/springframework/boot/logging/logback/base.xml"/>
    +  <logger name="com.zaxxer.hikari.pool" level="DEBUG"/>
    +</configuration>

    Errors like c.g.cloud.sql.core.SslSocketFactory : Re-throwing cached exception due to attempt to refresh instance information too soon after error

    If you see a lot of errors like this in a loop and can’t connect to your database, this is usually a symptom that something isn’t right with the permissions of your credentials or the Google Cloud SQL API is not enabled. +Verify that the Google Cloud SQL API is enabled in the Cloud Console and that your service account has the necessary IAM roles.

    To find out what’s causing the issue, you can enable DEBUG logging level as mentioned above.

    PostgreSQL: java.net.SocketException: already connected issue

    We found this exception to be common if your Maven project’s parent is spring-boot version 1.5.x, or in any other circumstance that would cause the version of the org.postgresql:postgresql dependency to be an older one (e.g., 9.4.1212.jre7).

    To fix this, re-declare the dependency in its correct version. +For example, in Maven:

    <dependency>
    +  <groupId>org.postgresql</groupId>
    +  <artifactId>postgresql</artifactId>
    +  <version>42.1.1</version>
    +</dependency>

    158. Spring Integration

    Spring Cloud GCP provides Spring Integration adapters that allow your applications to use Enterprise Integration Patterns backed up by Google Cloud Platform services.

    158.1 Channel Adapters for Cloud Pub/Sub

    The channel adapters for Google Cloud Pub/Sub connect your Spring MessageChannels to Google Cloud Pub/Sub topics and subscriptions. +This enables messaging between different processes, applications or micro-services backed up by Google Cloud Pub/Sub.

    The Spring Integration Channel Adapters for Google Cloud Pub/Sub are included in the spring-cloud-gcp-pubsub module.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-pubsub</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.integration</groupId>
    +    <artifactId>spring-integration-core</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub'
    +    compile group: 'org.springframework.integration', name: 'spring-integration-core'
    +}

    158.1.1 Inbound channel adapter

    PubSubInboundChannelAdapter is the inbound channel adapter for GCP Pub/Sub that listens to a GCP Pub/Sub subscription for new messages. +It converts new messages to an internal Spring Message and then sends it to the bound output channel.

    Google Pub/Sub treats message payloads as byte arrays. +So, by default, the inbound channel adapter will construct the Spring Message with byte[] as the payload. +However, you can change the desired payload type by setting the payloadType property of the PubSubInboundChannelAdapter. +The PubSubInboundChannelAdapter delegates the conversion to the desired payload type to the PubSubMessageConverter configured in the PubSubTemplate.

    To use the inbound channel adapter, a PubSubInboundChannelAdapter must be provided and configured on the user application side.

    @Bean
    +public MessageChannel pubsubInputChannel() {
    +    return new PublishSubscribeChannel();
    +}
    +
    +@Bean
    +public PubSubInboundChannelAdapter messageChannelAdapter(
    +    @Qualifier("pubsubInputChannel") MessageChannel inputChannel,
    +    SubscriberFactory subscriberFactory) {
    +    PubSubInboundChannelAdapter adapter =
    +        new PubSubInboundChannelAdapter(subscriberFactory, "subscriptionName");
    +    adapter.setOutputChannel(inputChannel);
    +    adapter.setAckMode(AckMode.MANUAL);
    +
    +    return adapter;
    +}

    In the example, we first specify the MessageChannel where the adapter is going to write incoming messages to. +The MessageChannel implementation isn’t important here. +Depending on your use case, you might want to use a MessageChannel other than PublishSubscribeChannel.

    Then, we declare a PubSubInboundChannelAdapter bean. +It requires the channel we just created and a SubscriberFactory, which creates Subscriber objects from the Google Cloud Java Client for Pub/Sub. +The Spring Boot starter for GCP Pub/Sub provides a configured SubscriberFactory.

    The PubSubInboundChannelAdapter supports three acknowledgement modes, with AckMode.AUTO being the default value;

    Automatic acking (AckMode.AUTO)

    A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown. +If a RuntimeException is thrown while the message is processed, then the message is nacked.

    Automatic acking OK (AckMode.AUTO_ACK)

    A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown. +If a RuntimeException is thrown while the message is processed, then the message is neither acked / nor nacked.

    This is useful when using the subscription’s ack deadline timeout as a retry delivery backoff mechanism.

    Manually acking (AckMode.MANUAL)

    The adapter attaches a BasicAcknowledgeablePubsubMessage object to the Message headers. +Users can extract the BasicAcknowledgeablePubsubMessage using the GcpPubSubHeaders.ORIGINAL_MESSAGE key and use it to (n)ack a message.

    @Bean
    +@ServiceActivator(inputChannel = "pubsubInputChannel")
    +public MessageHandler messageReceiver() {
    +    return message -> {
    +        LOGGER.info("Message arrived! Payload: " + new String((byte[]) message.getPayload()));
    +        BasicAcknowledgeablePubsubMessage originalMessage =
    +              message.getHeaders().get(GcpPubSubHeaders.ORIGINAL_MESSAGE, BasicAcknowledgeablePubsubMessage.class);
    +        originalMessage.ack();
    +    };
    +}

    158.1.2 Outbound channel adapter

    PubSubMessageHandler is the outbound channel adapter for GCP Pub/Sub that listens for new messages on a Spring MessageChannel. +It uses PubSubTemplate to post them to a GCP Pub/Sub topic.

    To construct a Pub/Sub representation of the message, the outbound channel adapter needs to convert the Spring Message payload to a byte array representation expected by Pub/Sub. +It delegates this conversion to the PubSubTemplate. +To customize the conversion, you can specify a PubSubMessageConverter in the PubSubTemplate that should convert the Object payload and headers of the Spring Message to a PubsubMessage.

    To use the outbound channel adapter, a PubSubMessageHandler bean must be provided and configured on the user application side.

    @Bean
    +@ServiceActivator(inputChannel = "pubsubOutputChannel")
    +public MessageHandler messageSender(PubSubTemplate pubsubTemplate) {
    +    return new PubSubMessageHandler(pubsubTemplate, "topicName");
    +}

    The provided PubSubTemplate contains all the necessary configuration to publish messages to a GCP Pub/Sub topic.

    PubSubMessageHandler publishes messages asynchronously by default. +A publish timeout can be configured for synchronous publishing. +If none is provided, the adapter waits indefinitely for a response.

    It is possible to set user-defined callbacks for the publish() call in PubSubMessageHandler through the setPublishFutureCallback() method. +These are useful to process the message ID, in case of success, or the error if any was thrown.

    To override the default destination you can use the GcpPubSubHeaders.DESTINATION header.

    @Autowired
    +private MessageChannel pubsubOutputChannel;
    +
    +public void handleMessage(Message<?> msg) throws MessagingException {
    +    final Message<?> message = MessageBuilder
    +        .withPayload(msg.getPayload())
    +        .setHeader(GcpPubSubHeaders.TOPIC, "customTopic").build();
    +    pubsubOutputChannel.send(message);
    +}

    It is also possible to set an SpEL expression for the topic with the setTopicExpression() or setTopicExpressionString() methods.

    158.1.3 Header mapping

    These channel adapters contain header mappers that allow you to map, or filter out, headers from Spring to Google Cloud Pub/Sub messages, and vice-versa. +By default, the inbound channel adapter maps every header on the Google Cloud Pub/Sub messages to the Spring messages produced by the adapter. +The outbound channel adapter maps every header from Spring messages into Google Cloud Pub/Sub ones, except the ones added by Spring, like headers with key "id", "timestamp" and "gcp_pubsub_acknowledgement". +In the process, the outbound mapper also converts the value of the headers into string.

    Each adapter declares a setHeaderMapper() method to let you further customize which headers you want to map from Spring to Google Cloud Pub/Sub, and vice-versa.

    For example, to filter out headers "foo", "bar" and all headers starting with the prefix "prefix_", you can use setHeaderMapper() along with the PubSubHeaderMapper implementation provided by this module.

    PubSubMessageHandler adapter = ...
    +...
    +PubSubHeaderMapper headerMapper = new PubSubHeaderMapper();
    +headerMapper.setOutboundHeaderPatterns("!foo", "!bar", "!prefix_*", "*");
    +adapter.setHeaderMapper(headerMapper);
    [Note]Note

    The order in which the patterns are declared in PubSubHeaderMapper.setOutboundHeaderPatterns() and PubSubHeaderMapper.setInboundHeaderPatterns() matters. +The first patterns have precedence over the following ones.

    In the previous example, the "*" pattern means every header is mapped. +However, because it comes last in the list, the previous patterns take precedence.

    158.3 Channel Adapters for Google Cloud Storage

    The channel adapters for Google Cloud Storage allow you to read and write files to Google Cloud Storage through MessageChannels.

    Spring Cloud GCP provides two inbound adapters, GcsInboundFileSynchronizingMessageSource and GcsStreamingMessageSource, and one outbound adapter, GcsMessageHandler.

    The Spring Integration Channel Adapters for Google Cloud Storage are included in the spring-cloud-gcp-storage module.

    To use the Storage portion of Spring Integration for Spring Cloud GCP, you must also provide the spring-integration-file dependency, since it isn’t pulled transitively.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-storage</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.integration</groupId>
    +    <artifactId>spring-integration-file</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage'
    +    compile group: 'org.springframework.integration', name: 'spring-integration-file'
    +}

    158.3.1 Inbound channel adapter

    The Google Cloud Storage inbound channel adapter polls a Google Cloud Storage bucket for new files and sends each of them in a Message payload to the MessageChannel specified in the @InboundChannelAdapter annotation. +The files are temporarily stored in a folder in the local file system.

    Here is an example of how to configure a Google Cloud Storage inbound channel adapter.

    @Bean
    +@InboundChannelAdapter(channel = "new-file-channel", poller = @Poller(fixedDelay = "5000"))
    +public MessageSource<File> synchronizerAdapter(Storage gcs) {
    +  GcsInboundFileSynchronizer synchronizer = new GcsInboundFileSynchronizer(gcs);
    +  synchronizer.setRemoteDirectory("your-gcs-bucket");
    +
    +  GcsInboundFileSynchronizingMessageSource synchAdapter =
    +          new GcsInboundFileSynchronizingMessageSource(synchronizer);
    +  synchAdapter.setLocalDirectory(new File("local-directory"));
    +
    +  return synchAdapter;
    +}

    158.3.2 Inbound streaming channel adapter

    The inbound streaming channel adapter is similar to the normal inbound channel adapter, except it does not require files to be stored in the file system.

    Here is an example of how to configure a Google Cloud Storage inbound streaming channel adapter.

    @Bean
    +@InboundChannelAdapter(channel = "streaming-channel", poller = @Poller(fixedDelay = "5000"))
    +public MessageSource<InputStream> streamingAdapter(Storage gcs) {
    +  GcsStreamingMessageSource adapter =
    +          new GcsStreamingMessageSource(new GcsRemoteFileTemplate(new GcsSessionFactory(gcs)));
    +  adapter.setRemoteDirectory("your-gcs-bucket");
    +  return adapter;
    +}

    158.3.3 Outbound channel adapter

    The outbound channel adapter allows files to be written to Google Cloud Storage. +When it receives a Message containing a payload of type File, it writes that file to the Google Cloud Storage bucket specified in the adapter.

    Here is an example of how to configure a Google Cloud Storage outbound channel adapter.

    @Bean
    +@ServiceActivator(inputChannel = "writeFiles")
    +public MessageHandler outboundChannelAdapter(Storage gcs) {
    +  GcsMessageHandler outboundChannelAdapter = new GcsMessageHandler(new GcsSessionFactory(gcs));
    +  outboundChannelAdapter.setRemoteDirectoryExpression(new ValueExpression<>("your-gcs-bucket"));
    +
    +  return outboundChannelAdapter;
    +}

    158.4 Sample

    A sample application is available.

    159. Spring Cloud Stream

    Spring Cloud GCP provides a Spring Cloud Stream binder to Google Cloud Pub/Sub.

    The provided binder relies on the Spring Integration Channel Adapters for Google Cloud Pub/Sub.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-pubsub-stream-binder</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub-stream-binder'
    +}

    159.1 Overview

    This binder binds producers to Google Cloud Pub/Sub topics and consumers to subscriptions.

    [Note]Note

    Partitioning is currently not supported by this binder.

    159.2 Configuration

    You can configure the Spring Cloud Stream Binder for Google Cloud Pub/Sub to automatically generate the underlying resources, like the Google Cloud Pub/Sub topics and subscriptions for producers and consumers. +For that, you can use the spring.cloud.stream.gcp.pubsub.bindings.<channelName>.<consumer|producer>.auto-create-resources property, which is turned ON by default.

    Starting with version 1.1, these and other binder properties can be configured globally for all the bindings, e.g. spring.cloud.stream.gcp.pubsub.default.consumer.auto-create-resources.

    If you are using Pub/Sub auto-configuration from the Spring Cloud GCP Pub/Sub Starter, you should refer to the configuration section for other Pub/Sub parameters.

    [Note]Note

    To use this binder with a running emulator, configure its host and port via spring.cloud.gcp.pubsub.emulator-host.

    159.2.1 Producer Destination Configuration

    If automatic resource creation is turned ON and the topic corresponding to the destination name does not exist, it will be created.

    For example, for the following configuration, a topic called myEvents would be created.

    application.properties.  +

    spring.cloud.stream.bindings.events.destination=myEvents
    +spring.cloud.stream.gcp.pubsub.bindings.events.producer.auto-create-resources=true

    +

    159.2.2 Consumer Destination Configuration

    If automatic resource creation is turned ON and the subscription and/or the topic do not exist for a consumer, a subscription and potentially a topic will be created. +The topic name will be the same as the destination name, and the subscription name will be the destination name followed by the consumer group name.

    Regardless of the auto-create-resources setting, if the consumer group is not specified, an anonymous one will be created with the name anonymous.<destinationName>.<randomUUID>. +Then when the binder shuts down, all Pub/Sub subscriptions created for anonymous consumer groups will be automatically cleaned up.

    For example, for the following configuration, a topic named myEvents and a subscription called myEvents.counsumerGroup1 would be created. +If the consumer group is not specified, a subscription called anonymous.myEvents.a6d83782-c5a3-4861-ac38-e6e2af15a7be would be created and later cleaned up.

    [Important]Important

    If you are manually creating Pub/Sub subscriptions for consumers, make sure that they follow the naming convention of <destinationName>.<consumerGroup>.

    application.properties.  +

    spring.cloud.stream.bindings.events.destination=myEvents
    +spring.cloud.stream.gcp.pubsub.bindings.events.consumer.auto-create-resources=true
    +
    +# specify consumer group, and avoid anonymous consumer group generation
    +spring.cloud.stream.bindings.events.group=consumerGroup1

    +

    159.3 Sample

    A sample application is available.

    160. Spring Cloud Sleuth

    Spring Cloud Sleuth is an instrumentation framework for Spring Boot applications. +It captures trace information and can forward traces to services like Zipkin for storage and analysis.

    Google Cloud Platform provides its own managed distributed tracing service called Stackdriver Trace. +Instead of running and maintaining your own Zipkin instance and storage, you can use Stackdriver Trace to store traces, view trace details, generate latency distributions graphs, and generate performance regression reports.

    This Spring Cloud GCP starter can forward Spring Cloud Sleuth traces to Stackdriver Trace without an intermediary Zipkin server.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-trace</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-trace'
    +}

    You must enable Stackdriver Trace API from the Google Cloud Console in order to capture traces. +Navigate to the Stackdriver Trace API for your project and make sure it’s enabled.

    [Note]Note

    If you are already using a Zipkin server capturing trace information from multiple platform/frameworks, you can also use a Stackdriver Zipkin proxy to forward those traces to Stackdriver Trace without modifying existing applications.

    160.1 Tracing

    Spring Cloud Sleuth uses the Brave tracer to generate traces. +This integration enables Brave to use the StackdriverTracePropagation propagation.

    A propagation is responsible for extracting trace context from an entity (e.g., an HTTP servlet request) and injecting trace context into an entity. +A canonical example of the propagation usage is a web server that receives an HTTP request, which triggers other HTTP requests from the server before returning an HTTP response to the original caller. +In the case of StackdriverTracePropagation, first it looks for trace context in the x-cloud-trace-context key (e.g., an HTTP request header). +The value of the x-cloud-trace-context key can be formatted in three different ways:

    • x-cloud-trace-context: TRACE_ID
    • x-cloud-trace-context: TRACE_ID/SPAN_ID
    • x-cloud-trace-context: TRACE_ID/SPAN_ID;o=TRACE_TRUE

    TRACE_ID is a 32-character hexadecimal value that encodes a 128-bit number.

    SPAN_ID is an unsigned long. +Since Stackdriver Trace doesn’t support span joins, a new span ID is always generated, regardless of the one specified in x-cloud-trace-context.

    TRACE_TRUE can either be 0 if the entity should be untraced, or 1 if it should be traced. +This field forces the decision of whether or not to trace the request; if omitted then the decision is deferred to the sampler.

    If a x-cloud-trace-context key isn’t found, StackdriverTracePropagation falls back to tracing with the X-B3 headers.

    160.2 Spring Boot Starter for Stackdriver Trace

    Spring Boot Starter for Stackdriver Trace uses Spring Cloud Sleuth and auto-configures a StackdriverSender that sends the Sleuth’s trace information to Stackdriver Trace.

    All configurations are optional:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.trace.enabled

    Auto-configure Spring Cloud Sleuth to send traces to Stackdriver Trace.

    No

    true

    spring.cloud.gcp.trace.project-id

    Overrides the project ID from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.credentials.location

    Overrides the credentials location from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.credentials.encoded-key

    Overrides the credentials encoded key from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.credentials.scopes

    Overrides the credentials scopes from the Spring Cloud GCP Module

    No

     

    spring.cloud.gcp.trace.num-executor-threads

    Number of threads used by the Trace executor

    No

    4

    spring.cloud.gcp.trace.authority

    HTTP/2 authority the channel claims to be connecting to.

    No

     

    spring.cloud.gcp.trace.compression

    Name of the compression to use in Trace calls

    No

     

    spring.cloud.gcp.trace.deadline-ms

    Call deadline in milliseconds

    No

     

    spring.cloud.gcp.trace.max-inbound-size

    Maximum size for inbound messages

    No

     

    spring.cloud.gcp.trace.max-outbound-size

    Maximum size for outbound messages

    No

     

    spring.cloud.gcp.trace.wait-for-ready

    Waits for the channel to be ready in case of a transient failure

    No

    false

    spring.cloud.gcp.trace.messageTimeout

    Timeout in seconds before pending spans will be sent in batches to GCP Stackdriver Trace. Added for forward compatibility.

    No

    spring.zipkin.messageTimeout

    You can use core Spring Cloud Sleuth properties to control Sleuth’s sampling rate, etc. +Read Sleuth documentation for more information on Sleuth configurations.

    For example, when you are testing to see the traces are going through, you can set the sampling rate to 100%.

    spring.sleuth.sampler.probability=1                     # Send 100% of the request traces to Stackdriver.
    +spring.sleuth.web.skipPattern=(^cleanup.*|.+favicon.*)  # Ignore some URL paths.

    Spring Cloud GCP Trace does override some Sleuth configurations:

    • Always uses 128-bit Trace IDs. +This is required by Stackdriver Trace.
    • Does not use Span joins. +Span joins will share the span ID between the client and server Spans. +Stackdriver requires that every Span ID within a Trace to be unique, so Span joins are not supported.
    • Uses StackdriverHttpClientParser and StackdriverHttpServerParser by default to populate Stackdriver related fields.

    160.3 Overriding the auto-configuration

    Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. +In order to get this to work, every tracing system needs to have a Reporter<Span> and Sender. +If you want to override the provided beans you need to give them a specific name. +To do this you can use respectively StackdriverTraceAutoConfiguration.REPORTER_BEAN_NAME and StackdriverTraceAutoConfiguration.SENDER_BEAN_NAME.

    160.4 Integration with Logging

    Integration with Stackdriver Logging is available through the Stackdriver Logging Support. +If the Trace integration is used together with the Logging one, the request logs will be associated to the corresponding traces. +The trace logs can be viewed by going to the Google Cloud Console Trace List, selecting a trace and pressing the Logs → View link in the Details section.

    160.5 Sample

    A sample application and a codelab are available.

    161. Stackdriver Logging

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-logging</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-logging'
    +}

    Stackdriver Logging is the managed logging service provided by Google Cloud Platform.

    This module provides support for associating a web request trace ID with the corresponding log entries. +It does so by retrieving the X-B3-TraceId value from the Mapped Diagnostic Context (MDC), which is set by Spring Cloud Sleuth. +If Spring Cloud Sleuth isn’t used, the configured TraceIdExtractor extracts the desired header value and sets it as the log entry’s trace ID. +This allows grouping of log messages by request, for example, in the Google Cloud Console Logs viewer.

    [Note]Note

    Due to the way logging is set up, the GCP project ID and credentials defined in application.properties are ignored. +Instead, you should set the GOOGLE_CLOUD_PROJECT and GOOGLE_APPLICATION_CREDENTIALS environment variables to the project ID and credentials private key location, respectively. +You can do this easily if you’re using the Google Cloud SDK, using the gcloud config set project [YOUR_PROJECT_ID] and gcloud auth application-default login commands, respectively.

    161.1 Web MVC Interceptor

    For use in Web MVC-based applications, TraceIdLoggingWebMvcInterceptor is provided that extracts the request trace ID from an HTTP request using a TraceIdExtractor and stores it in a thread-local, which can then be used in a logging appender to add the trace ID metadata to log messages.

    [Warning]Warning

    If Spring Cloud GCP Trace is enabled, the logging module disables itself and delegates log correlation to Spring Cloud Sleuth.

    LoggingWebMvcConfigurer configuration class is also provided to help register the TraceIdLoggingWebMvcInterceptor in Spring MVC applications.

    Applications hosted on the Google Cloud Platform include trace IDs under the x-cloud-trace-context header, which will be included in log entries. +However, if Sleuth is used the trace ID will be picked up from the MDC.

    161.2 Logback Support

    Currently, only Logback is supported and there are 2 possibilities to log to Stackdriver via this library with Logback: via direct API calls and through JSON-formatted console logs.

    161.2.1 Log via API

    A Stackdriver appender is available using org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml. +This appender builds a Stackdriver Logging log entry from a JUL or Logback log entry, adds a trace ID to it and sends it to Stackdriver Logging.

    STACKDRIVER_LOG_NAME and STACKDRIVER_LOG_FLUSH_LEVEL environment variables can be used to customize the STACKDRIVER appender.

    Your configuration may then look like this:

    <configuration>
    +  <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml" />
    +
    +  <root level="INFO">
    +    <appender-ref ref="STACKDRIVER" />
    +  </root>
    +</configuration>

    If you want to have more control over the log output, you can further configure the appender. +The following properties are available:

    PropertyDefault ValueDescription

    log

    spring.log

    The Stackdriver Log name. +This can also be set via the STACKDRIVER_LOG_NAME environmental variable.

    flushLevel

    WARN

    If a log entry with this level is encountered, trigger a flush of locally buffered log to Stackdriver Logging. +This can also be set via the STACKDRIVER_LOG_FLUSH_LEVEL environmental variable.

    161.2.2 Log via Console

    For Logback, a org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml file is made available for import to make it easier to configure the JSON Logback appender.

    Your configuration may then look something like this:

    <configuration>
    +  <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml" />
    +
    +  <root level="INFO">
    +    <appender-ref ref="CONSOLE_JSON" />
    +  </root>
    +</configuration>

    If your application is running on Google Kubernetes Engine, Google Compute Engine or Google App Engine Flexible, your console logging is automatically saved to Google Stackdriver Logging. +Therefore, you can just include org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml in your logging configuration, which logs JSON entries to the console. +The trace id will be set correctly.

    If you want to have more control over the log output, you can further configure the appender. +The following properties are available:

    PropertyDefault ValueDescription

    projectId

    If not set, default value is determined in the following order:

    +
    1. SPRING_CLOUD_GCP_LOGGING_PROJECT_ID Environmental Variable.
    2. Value of DefaultGcpProjectIdProvider.getProjectId()

    This is used to generate fully qualified Stackdriver Trace ID format: projects/[PROJECT-ID]/traces/[TRACE-ID].

    +

    This format is required to correlate trace between Stackdriver Trace and Stackdriver Logging.

    +

    If projectId is not set and cannot be determined, then it’ll log traceId without the fully qualified format.

    includeTraceId

    true

    Should the traceId be included

    includeSpanId

    true

    Should the spanId be included

    includeLevel

    true

    Should the severity be included

    includeThreadName

    true

    Should the thread name be included

    includeMDC

    true

    Should all MDC properties be included. +The MDC properties X-B3-TraceId, X-B3-SpanId and X-Span-Export provided by Spring Sleuth will get excluded as they get handled separately

    includeLoggerName

    true

    Should the name of the logger be included

    includeFormattedMessage

    true

    Should the formatted log message be included.

    includeExceptionInMessage

    true

    Should the stacktrace be appended to the formatted log message. +This setting is only evaluated if includeFormattedMessage is true

    includeContextName

    true

    Should the logging context be included

    includeMessage

    false

    Should the log message with blank placeholders be included

    includeException

    false

    Should the stacktrace be included as a own field

    This is an example of such an Logback configuration:

    <configuration >
    +  <property name="projectId" value="${projectId:-${GOOGLE_CLOUD_PROJECT}}"/>
    +
    +  <appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender">
    +    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
    +      <layout class="org.springframework.cloud.gcp.logging.StackdriverJsonLayout">
    +        <projectId>${projectId}</projectId>
    +
    +        <!--<includeTraceId>true</includeTraceId>-->
    +        <!--<includeSpanId>true</includeSpanId>-->
    +        <!--<includeLevel>true</includeLevel>-->
    +        <!--<includeThreadName>true</includeThreadName>-->
    +        <!--<includeMDC>true</includeMDC>-->
    +        <!--<includeLoggerName>true</includeLoggerName>-->
    +        <!--<includeFormattedMessage>true</includeFormattedMessage>-->
    +        <!--<includeExceptionInMessage>true</includeExceptionInMessage>-->
    +        <!--<includeContextName>true</includeContextName>-->
    +        <!--<includeMessage>false</includeMessage>-->
    +        <!--<includeException>false</includeException>-->
    +      </layout>
    +    </encoder>
    +  </appender>
    +</configuration>

    161.3 Sample

    A Sample Spring Boot Application is provided to show how to use the Cloud logging starter.

    162. Spring Cloud Config

    Spring Cloud GCP makes it possible to use the Google Runtime Configuration API as a Spring Cloud Config server to remotely store your application configuration data.

    The Spring Cloud GCP Config support is provided via its own Spring Boot starter. +It enables the use of the Google Runtime Configuration API as a source for Spring Boot configuration properties.

    [Note]Note

    The Google Cloud Runtime Configuration service is in beta status.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-config</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-config'
    +}

    162.1 Configuration

    The following parameters are configurable in Spring Cloud GCP Config:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.config.enabled

    Enables the Config client

    No

    false

    spring.cloud.gcp.config.name

    Name of your application

    No

    Value of the spring.application.name property. +If none, application

    spring.cloud.gcp.config.profile

    Active profile

    No

    Value of the spring.profiles.active property. +If more than a single profile, last one is chosen

    spring.cloud.gcp.config.timeout-millis

    Timeout in milliseconds for connecting to the Google Runtime Configuration API

    No

    60000

    spring.cloud.gcp.config.project-id

    GCP project ID where the Google Runtime Configuration API is hosted

    No

     

    spring.cloud.gcp.config.credentials.location

    OAuth2 credentials for authenticating with the Google Runtime Configuration API

    No

     

    spring.cloud.gcp.config.credentials.encoded-key

    Base64-encoded OAuth2 credentials for authenticating with the Google Runtime Configuration API

    No

     

    spring.cloud.gcp.config.credentials.scopes

    OAuth2 scope for Spring Cloud GCP Config credentials

    No

    https://www.googleapis.com/auth/cloudruntimeconfig

    [Note]Note

    These properties should be specified in a bootstrap.yml/bootstrap.properties file, rather than the usual applications.yml/application.properties.

    [Note]Note

    Core properties, as described in Spring Cloud GCP Core Module, do not apply to Spring Cloud GCP Config.

    162.2 Quick start

    1. Create a configuration in the Google Runtime Configuration API that is called ${spring.application.name}_${spring.profiles.active}. +In other words, if spring.application.name is myapp and spring.profiles.active is prod, the configuration should be called myapp_prod.

      In order to do that, you should have the Google Cloud SDK installed, own a Google Cloud Project and run the following command:

    gcloud init # if this is your first Google Cloud SDK run.
    +gcloud beta runtime-config configs create myapp_prod
    +gcloud beta runtime-config configs variables set myapp.queue-size 25 --config-name myapp_prod
    1. Configure your bootstrap.properties file with your application’s configuration data:

      spring.application.name=myapp
      +spring.profiles.active=prod
    2. Add the @ConfigurationProperties annotation to a Spring-managed bean:

      @Component
      +@ConfigurationProperties("myapp")
      +public class SampleConfig {
      +
      +  private int queueSize;
      +
      +  public int getQueueSize() {
      +    return this.queueSize;
      +  }
      +
      +  public void setQueueSize(int queueSize) {
      +    this.queueSize = queueSize;
      +  }
      +}

    When your Spring application starts, the queueSize field value will be set to 25 for the above SampleConfig bean.

    162.3 Refreshing the configuration at runtime

    Spring Cloud provides support to have configuration parameters be reloadable with the POST request to /actuator/refresh endpoint.

    1. Add the Spring Boot Actuator dependency:

    Maven coordinates:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-actuator</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator'
    +}
    1. Add @RefreshScope to your Spring configuration class to have parameters be reloadable at runtime.
    2. Add management.endpoints.web.exposure.include=refresh to your application.properties to allow unrestricted access to /actuator/refresh.
    3. Update a property with gcloud:

      $ gcloud beta runtime-config configs variables set \
      +  myapp.queue_size 200 \
      +  --config-name myapp_prod
    4. Send a POST request to the refresh endpoint:

      $ curl -XPOST https://myapp.host.com/actuator/refresh

    162.4 Sample

    A sample application and a codelab are available.

    163. Spring Data Cloud Spanner

    Spring Data is an abstraction for storing and retrieving POJOs in numerous storage technologies. +Spring Cloud GCP adds Spring Data support for Google Cloud Spanner.

    Maven coordinates for this module only, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-data-spanner</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-spanner'
    +}

    We provide a Spring Boot Starter for Spring Data Spanner, with which you can leverage our recommended auto-configuration setup. +To use the starter, see the coordinates see below.

    Maven:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-data-spanner</artifactId>
    +</dependency>

    Gradle:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-spanner'
    +}

    This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Spanner libraries as well.

    163.1 Configuration

    To setup Spring Data Cloud Spanner, you have to configure the following:

    • Setup the connection details to Google Cloud Spanner.
    • Enable Spring Data Repositories (optional).

    163.1.1 Cloud Spanner settings

    You can the use Spring Boot Starter for Spring Data Spanner to autoconfigure Google Cloud Spanner in your Spring application. +It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project. +The following configuration options are available:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.spanner.instance-id

    Cloud Spanner instance to use

    Yes

     

    spring.cloud.gcp.spanner.database

    Cloud Spanner database to use

    Yes

     

    spring.cloud.gcp.spanner.project-id

    GCP project ID where the Google Cloud Spanner API is hosted, if different from the one in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.spanner.credentials.location

    OAuth2 credentials for authenticating with the +Google Cloud Spanner API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.spanner.credentials.encoded-key

    Base64-encoded OAuth2 credentials for authenticating with the +Google Cloud Spanner API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.spanner.credentials.scopes

    OAuth2 scope for Spring Cloud GCP +Cloud Spanner credentials

    No

    https://www.googleapis.com/auth/spanner.data

    spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade

    If true, then schema statements generated by SpannerSchemaUtils for tables with interleaved parent-child relationships will be "ON DELETE CASCADE". +The schema for the tables will be "ON DELETE NO ACTION" if false.

    No

    true

    spring.cloud.gcp.spanner.numRpcChannels

    Number of gRPC channels used to connect to Cloud Spanner

    No

    4 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.prefetchChunks

    Number of chunks prefetched by Cloud Spanner for read and query

    No

    4 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.minSessions

    Minimum number of sessions maintained in the session pool

    No

    0 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.maxSessions

    Maximum number of sessions session pool can have

    No

    400 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.maxIdleSessions

    Maximum number of idle sessions session pool will maintain

    No

    0 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.writeSessionsFraction

    Fraction of sessions to be kept prepared for write transactions

    No

    0.2 - Determined by Cloud Spanner client library

    spring.cloud.gcp.spanner.keepAliveIntervalMinutes

    How long to keep idle sessions alive

    No

    30 - Determined by Cloud Spanner client library

    163.1.2 Repository settings

    Spring Data Repositories can be configured via the @EnableSpannerRepositories annotation on your main @Configuration class. +With our Spring Boot Starter for Spring Data Cloud Spanner, @EnableSpannerRepositories is automatically added. +It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by @EnableSpannerRepositories.

    163.1.3 Autoconfiguration

    Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:

    • an instance of SpannerTemplate
    • an instance of SpannerDatabaseAdminTemplate for generating table schemas from object hierarchies and creating and deleting tables and databases
    • an instance of all user-defined repositories extending SpannerRepository, CrudRepository, PagingAndSortingRepository, when repositories are enabled
    • an instance of DatabaseClient from the Google Cloud Java Client for Spanner, for convenience and lower level API access

    163.2 Object Mapping

    Spring Data Cloud Spanner allows you to map domain POJOs to Cloud Spanner tables via annotations:

    @Table(name = "traders")
    +public class Trader {
    +
    +	@PrimaryKey
    +	@Column(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@NotMapped
    +	Double temporaryNumber;
    +}

    Spring Data Cloud Spanner will ignore any property annotated with @NotMapped. +These properties will not be written to or read from Spanner.

    163.2.1 Constructors

    Simple constructors are supported on POJOs. +The constructor arguments can be a subset of the persistent properties. +Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument. +Arguments that are not directly set to properties are not supported.

    @Table(name = "traders")
    +public class Trader {
    +	@PrimaryKey
    +	@Column(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@NotMapped
    +	Double temporaryNumber;
    +
    +	public Trader(String traderId, String firstName) {
    +	    this.traderId = traderId;
    +	    this.firstName = firstName;
    +	}
    +}

    163.2.2 Table

    The @Table annotation can provide the name of the Cloud Spanner table that stores instances of the annotated class, one per row. +This annotation is optional, and if not given, the name of the table is inferred from the class name with the first character uncapitalized.

    SpEL expressions for table names

    In some cases, you might want the @Table table name to be determined dynamically. +To do that, you can use Spring Expression Language.

    For example:

    @Table(name = "trades_#{tableNameSuffix}")
    +public class Trade {
    +	// ...
    +}

    The table name will be resolved only if the tableNameSuffix value/bean in the Spring application context is defined. +For example, if tableNameSuffix has the value "123", the table name will resolve to trades_123.

    163.2.3 Primary Keys

    For a simple table, you may only have a primary key consisting of a single column. +Even in that case, the @PrimaryKey annotation is required. +@PrimaryKey identifies the one or more ID properties corresponding to the primary key.

    Spanner has first class support for composite primary keys of multiple columns. +You have to annotate all of your POJO’s fields that the primary key consists of with @PrimaryKey as below:

    @Table(name = "trades")
    +public class Trade {
    +	@PrimaryKey(keyOrder = 2)
    +	@Column(name = "trade_id")
    +	private String tradeId;
    +
    +	@PrimaryKey(keyOrder = 1)
    +	@Column(name = "trader_id")
    +	private String traderId;
    +
    +	private String action;
    +
    +	private Double price;
    +
    +	private Double shares;
    +
    +	private String symbol;
    +}

    The keyOrder parameter of @PrimaryKey identifies the properties corresponding to the primary key columns in order, starting with 1 and increasing consecutively. +Order is important and must reflect the order defined in the Cloud Spanner schema. +In our example the DDL to create the table and its primary key is as follows:

    CREATE TABLE trades (
    +    trader_id STRING(MAX),
    +    trade_id STRING(MAX),
    +    action STRING(15),
    +    symbol STRING(10),
    +    price FLOAT64,
    +    shares FLOAT64
    +) PRIMARY KEY (trader_id, trade_id)

    Spanner does not have automatic ID generation. +For most use-cases, sequential IDs should be used with caution to avoid creating data hotspots in the system. +Read Spanner Primary Keys documentation for a better understanding of primary keys and recommended practices.

    163.2.4 Columns

    All accessible properties on POJOs are automatically recognized as a Cloud Spanner column. +Column naming is generated by the PropertyNameFieldNamingStrategy by default defined on the SpannerMappingContext bean. +The @Column annotation optionally provides a different column name than that of the property and some other settings:

    • name is the optional name of the column
    • spannerTypeMaxLength specifies for STRING and BYTES columns the maximum length. +This setting is only used when generating DDL schema statements based on domain types.
    • nullable specifies if the column is created as NOT NULL. +This setting is only used when generating DDL schema statements based on domain types.
    • spannerType is the Cloud Spanner column type you can optionally specify. +If this is not specified then a compatible column type is inferred from the Java property type.
    • spannerCommitTimestamp is a boolean specifying if this property corresponds to an auto-populated commit timestamp column. +Any value set in this property will be ignored when writing to Cloud Spanner.

    163.2.5 Embedded Objects

    If an object of type B is embedded as a property of A, then the columns of B will be saved in the same Cloud Spanner table as those of A.

    If B has primary key columns, those columns will be included in the primary key of A. B can also have embedded properties. +Embedding allows reuse of columns between multiple entities, and can be useful for implementing parent-child situations, because Cloud Spanner requires child tables to include the key columns of their parents.

    For example:

    class X {
    +  @PrimaryKey
    +  String grandParentId;
    +
    +  long age;
    +}
    +
    +class A {
    +  @PrimaryKey
    +  @Embedded
    +  X grandParent;
    +
    +  @PrimaryKey(keyOrder = 2)
    +  String parentId;
    +
    +  String value;
    +}
    +
    +@Table(name = "items")
    +class B {
    +  @PrimaryKey
    +  @Embedded
    +  A parent;
    +
    +  @PrimaryKey(keyOrder = 2)
    +  String id;
    +
    +  @Column(name = "child_value")
    +  String value;
    +}

    Entities of B can be stored in a table defined as:

    CREATE TABLE items (
    +    grandParentId STRING(MAX),
    +    parentId STRING(MAX),
    +    id STRING(MAX),
    +    value STRING(MAX),
    +    child_value STRING(MAX),
    +    age INT64
    +) PRIMARY KEY (grandParentId, parentId, id)

    Note that embedded properties' column names must all be unique.

    163.2.6 Relationships

    Spring Data Cloud Spanner supports parent-child relationships using the Cloud Spanner parent-child interleaved table mechanism. +Cloud Spanner interleaved tables enforce the one-to-many relationship and provide efficient queries and operations on entities of a single domain parent entity. +These relationships can be up to 7 levels deep. +Cloud Spanner also provides automatic cascading delete or enforces the deletion of child entities before parents.

    While one-to-one and many-to-many relationships can be implemented in Cloud Spanner and Spring Data Cloud Spanner using constructs of interleaved parent-child tables, only the parent-child relationship is natively supported. +Cloud Spanner does not support the foreign key constraint, though the parent-child key constraint enforces a similar requirement when used with interleaved tables.

    For example, the following Java entities:

    @Table(name = "Singers")
    +class Singer {
    +  @PrimaryKey
    +  long SingerId;
    +
    +  String FirstName;
    +
    +  String LastName;
    +
    +  byte[] SingerInfo;
    +
    +  @Interleaved
    +  List<Album> albums;
    +}
    +
    +@Table(name = "Albums")
    +class Album {
    +  @PrimaryKey
    +  long SingerId;
    +
    +  @PrimaryKey(keyOrder = 2)
    +  long AlbumId;
    +
    +  String AlbumTitle;
    +}

    These classes can correspond to an existing pair of interleaved tables. +The @Interleaved annotation may be applied to Collection properties and the inner type is resolved as the child entity type. +The schema needed to create them can also be generated using the SpannerSchemaUtils and executed using the SpannerDatabaseAdminTemplate:

    @Autowired
    +SpannerSchemaUtils schemaUtils;
    +
    +@Autowired
    +SpannerDatabaseAdminTemplate databaseAdmin;
    +...
    +
    +// Get the create statmenets for all tables in the table structure rooted at Singer
    +List<String> createStrings = this.schemaUtils.getCreateTableDdlStringsForInterleavedHierarchy(Singer.class);
    +
    +// Create the tables and also create the database if necessary
    +this.databaseAdmin.executeDdlStrings(createStrings, true);

    The createStrings list contains table schema statements using column names and types compatible with the provided Java type and any resolved child relationship types contained within based on the configured custom converters.

    CREATE TABLE Singers (
    +  SingerId   INT64 NOT NULL,
    +  FirstName  STRING(1024),
    +  LastName   STRING(1024),
    +  SingerInfo BYTES(MAX),
    +) PRIMARY KEY (SingerId);
    +
    +CREATE TABLE Albums (
    +  SingerId     INT64 NOT NULL,
    +  AlbumId      INT64 NOT NULL,
    +  AlbumTitle   STRING(MAX),
    +) PRIMARY KEY (SingerId, AlbumId),
    +  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

    The ON DELETE CASCADE clause indicates that Cloud Spanner will delete all Albums of a singer if the Singer is deleted. +The alternative is ON DELETE NO ACTION, where a Singer cannot be deleted until all of its Albums have already been deleted. +When using SpannerSchemaUtils to generate the schema strings, the spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade boolean setting determines if these schema are generated as ON DELETE CASCADE for true and ON DELETE NO ACTION for false.

    Cloud Spanner restricts these relationships to 7 child layers. +A table may have multiple child tables.

    On updating or inserting an object to Cloud Spanner, all of its referenced children objects are also updated or inserted in the same request, respectively. +On read, all of the interleaved child rows are also all read.

    163.2.7 Supported Types

    Spring Data Cloud Spanner natively supports the following types for regular fields but also utilizes custom converters (detailed in following sections) and dozens of pre-defined Spring Data custom converters to handle other common Java types.

    Natively supported types:

    • com.google.cloud.ByteArray
    • com.google.cloud.Date
    • com.google.cloud.Timestamp
    • java.lang.Boolean, boolean
    • java.lang.Double, double
    • java.lang.Long, long
    • java.lang.Integer, int
    • java.lang.String
    • double[]
    • long[]
    • boolean[]
    • java.util.Date
    • java.util.Instant
    • java.sql.Date

    163.2.8 Lists

    Spanner supports ARRAY types for columns. +ARRAY columns are mapped to List fields in POJOS.

    Example:

    List<Double> curve;

    The types inside the lists can be any singular property type.

    163.2.9 Lists of Structs

    Cloud Spanner queries can construct STRUCT values that appear as columns in the result. +Cloud Spanner requires STRUCT values appear in ARRAYs at the root level: SELECT ARRAY(SELECT STRUCT(1 as val1, 2 as val2)) as pair FROM Users.

    Spring Data Cloud Spanner will attempt to read the column STRUCT values into a property that is an Iterable of an entity type compatible with the schema of the column STRUCT value.

    For the previous array-select example, the following property can be mapped with the constructed ARRAY<STRUCT> column: List<TwoInts> pair; where the TwoInts type is defined:

    class TwoInts {
    +
    +  int val1;
    +
    +  int val2;
    +}

    163.2.10 Custom types

    Custom converters can be used to extend the type support for user defined types.

    1. Converters need to implement the org.springframework.core.convert.converter.Converter interface in both directions.
    2. The user defined type needs to be mapped to one of the basic types supported by Spanner:

      • com.google.cloud.ByteArray
      • com.google.cloud.Date
      • com.google.cloud.Timestamp
      • java.lang.Boolean, boolean
      • java.lang.Double, double
      • java.lang.Long, long
      • java.lang.String
      • double[]
      • long[]
      • boolean[]
      • enum types
    3. An instance of both Converters needs to be passed to a ConverterAwareMappingSpannerEntityProcessor, which then has to be made available as a @Bean for SpannerEntityProcessor.

    For example:

    We would like to have a field of type Person on our Trade POJO:

    @Table(name = "trades")
    +public class Trade {
    +  //...
    +  Person person;
    +  //...
    +}

    Where Person is a simple class:

    public class Person {
    +
    +  public String firstName;
    +  public String lastName;
    +
    +}

    We have to define the two converters:

      public class PersonWriteConverter implements Converter<Person, String> {
    +
    +    @Override
    +    public String convert(Person person) {
    +      return person.firstName + " " + person.lastName;
    +    }
    +  }
    +
    +  public class PersonReadConverter implements Converter<String, Person> {
    +
    +    @Override
    +    public Person convert(String s) {
    +      Person person = new Person();
    +      person.firstName = s.split(" ")[0];
    +      person.lastName = s.split(" ")[1];
    +      return person;
    +    }
    +  }

    That will be configured in our @Configuration file:

    @Configuration
    +public class ConverterConfiguration {
    +
    +	@Bean
    +	public SpannerEntityProcessor spannerEntityProcessor(SpannerMappingContext spannerMappingContext) {
    +		return new ConverterAwareMappingSpannerEntityProcessor(spannerMappingContext,
    +				Arrays.asList(new PersonWriteConverter()),
    +				Arrays.asList(new PersonReadConverter()));
    +	}
    +}

    163.2.11 Custom Converter for Struct Array Columns

    If a Converter<Struct, A> is provided, then properties of type List<A> can be used in your entity types.

    163.3 Spanner Operations & Template

    SpannerOperations and its implementation, SpannerTemplate, provides the Template pattern familiar to Spring developers. +It provides:

    • Resource management
    • One-stop-shop to Spanner operations with the Spring Data POJO mapping and conversion features
    • Exception conversion

    Using the autoconfigure provided by our Spring Boot Starter for Spanner, your Spring application context will contain a fully configured SpannerTemplate object that you can easily autowire in your application:

    @SpringBootApplication
    +public class SpannerTemplateExample {
    +
    +	@Autowired
    +	SpannerTemplate spannerTemplate;
    +
    +	public void doSomething() {
    +		this.spannerTemplate.delete(Trade.class, KeySet.all());
    +		//...
    +		Trade t = new Trade();
    +		//...
    +		this.spannerTemplate.insert(t);
    +		//...
    +		List<Trade> tradesByAction = spannerTemplate.findAll(Trade.class);
    +		//...
    +	}
    +}

    The Template API provides convenience methods for:

    • Reads, and by providing SpannerReadOptions and +SpannerQueryOptions

      • Stale read
      • Read with secondary indices
      • Read with limits and offsets
      • Read with sorting
    • Queries
    • DML operations (delete, insert, update, upsert)
    • Partial reads

      • You can define a set of columns to be read into your entity
    • Partial writes

      • Persist only a few properties from your entity
    • Read-only transactions
    • Locking read-write transactions

    163.3.1 SQL Query

    Cloud Spanner has SQL support for running read-only queries. +All the query related methods start with query on SpannerTemplate. +Using SpannerTemplate you can execute SQL queries that map to POJOs:

    List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"));

    163.3.2 Read

    Spanner exposes a Read API for reading single row or multiple rows in a table or in a secondary index.

    Using SpannerTemplate you can execute reads, for example:

    List<Trade> trades = this.spannerTemplate.readAll(Trade.class);

    Main benefit of reads over queries is reading multiple rows of a certain pattern of keys is much easier using the features of the KeySet class.

    163.3.3 Advanced reads

    Stale read

    All reads and queries are strong reads by default. +A strong read is a read at a current timestamp and is guaranteed to see all data that has been committed up until the start of this read. +A stale read on the other hand is read at a timestamp in the past. +Cloud Spanner allows you to determine how current the data should be when you read data. +With SpannerTemplate you can specify the Timestamp by setting it on SpannerQueryOptions or SpannerReadOptions to the appropriate read or query methods:

    Reads:

    // a read with options:
    +SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setTimestamp(Timestamp.now());
    +List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);

    Queries:

    // a query with options:
    +SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setTimestamp(Timestamp.now());
    +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);

    Read from a secondary index

    Using a secondary index is available for Reads via the Template API and it is also implicitly available via SQL for Queries.

    The following shows how to read rows from a table using a secondary index simply by setting index on SpannerReadOptions:

    SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setIndex("TradesByTrader");
    +List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions);

    Read with offsets and limits

    Limits and offsets are only supported by Queries. +The following will get only the first two rows of the query:

    SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setLimit(2).setOffset(3);
    +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);

    Note that the above is equivalent of executing SELECT * FROM trades LIMIT 2 OFFSET 3.

    Sorting

    Reads by keys do not support sorting. +However, queries on the Template API support sorting through standard SQL and also via Spring Data Sort API:

    List<Trade> trades = this.spannerTemplate.queryAll(Trade.class, Sort.by("action"));

    If the provided sorted field name is that of a property of the domain type, then the column name corresponding to that property will be used in the query. +Otherwise, the given field name is assumed to be the name of the column in the Cloud Spanner table. +Sorting on columns of Cloud Spanner types STRING and BYTES can be done while ignoring case:

    Sort.by(Order.desc("action").ignoreCase())

    Partial read

    Partial read is only possible when using Queries. +In case the rows returned by the query have fewer columns than the entity that it will be mapped to, Spring Data will map the returned columns only. +This setting also applies to nested structs and their corresponding nested POJO properties.

    List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT action, symbol FROM trades"),
    +    new SpannerQueryOptions().setAllowMissingResultSetColumns(true));

    If the setting is set to false, then an exception will be thrown if there are missing columns in the query result.

    Summary of options for Query vs Read

    Feature

    Query supports it

    Read supports it

    SQL

    yes

    no

    Partial read

    yes

    no

    Limits

    yes

    no

    Offsets

    yes

    no

    Secondary index

    yes

    yes

    Read using index range

    no

    yes

    Sorting

    yes

    no

    163.3.4 Write / Update

    The write methods of SpannerOperations accept a POJO and writes all of its properties to Spanner. +The corresponding Spanner table and entity metadata is obtained from the given object’s actual type.

    If a POJO was retrieved from Spanner and its primary key properties values were changed and then written or updated, the operation will occur as if against a row with the new primary key values. +The row with the original primary key values will not be affected.

    Insert

    The insert method of SpannerOperations accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if a row with the POJO’s primary key already exists in the table.

    Trade t = new Trade();
    +this.spannerTemplate.insert(t);

    Update

    The update method of SpannerOperations accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if the POJO’s primary key does not already exist in the table.

    // t was retrieved from a previous operation
    +this.spannerTemplate.update(t);

    Upsert

    The upsert method of SpannerOperations accepts a POJO and writes all of its properties to Spanner using update-or-insert.

    // t was retrieved from a previous operation or it's new
    +this.spannerTemplate.upsert(t);

    Partial Update

    The update methods of SpannerOperations operate by default on all properties within the given object, but also accept String[] and Optional<Set<String>> of column names. +If the Optional of set of column names is empty, then all columns are written to Spanner. +However, if the Optional is occupied by an empty set, then no columns will be written.

    // t was retrieved from a previous operation or it's new
    +this.spannerTemplate.update(t, "symbol", "action");

    163.3.5 DML

    DML statements can be executed using SpannerOperations.executeDmlStatement. +Inserts, updates, and deletions can affect any number of rows and entities.

    163.3.6 Transactions

    SpannerOperations provides methods to run java.util.Function objects within a single transaction while making available the read and write methods from SpannerOperations.

    Read/Write Transaction

    Read and write transactions are provided by SpannerOperations via the performReadWriteTransaction method:

    @Autowired
    +SpannerOperations mySpannerOperations;
    +
    +public String doWorkInsideTransaction() {
    +  return mySpannerOperations.performReadWriteTransaction(
    +    transActionSpannerOperations -> {
    +      // Work with transActionSpannerOperations here.
    +      // It is also a SpannerOperations object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    The performReadWriteTransaction method accepts a Function that is provided an instance of a SpannerOperations object. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular SpannerOperations with a few exceptions:

    • Its read functionality cannot perform stale reads, because all reads and writes happen at the single point in time of the transaction.
    • It cannot perform sub-transactions via performReadWriteTransaction or performReadOnlyTransaction.

    As these read-write transactions are locking, it is recommended that you use the performReadOnlyTransaction if your function does not perform any writes.

    Read-only Transaction

    The performReadOnlyTransaction method is used to perform read-only transactions using a SpannerOperations:

    @Autowired
    +SpannerOperations mySpannerOperations;
    +
    +public String doWorkInsideTransaction() {
    +  return mySpannerOperations.performReadOnlyTransaction(
    +    transActionSpannerOperations -> {
    +      // Work with transActionSpannerOperations here.
    +      // It is also a SpannerOperations object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    The performReadOnlyTransaction method accepts a Function that is provided an instance of a +SpannerOperations object. +This method also accepts a ReadOptions object, but the only attribute used is the timestamp used to determine the snapshot in time to perform the reads in the transaction. +If the timestamp is not set in the read options the transaction is run against the current state of the database. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular SpannerOperations with +a few exceptions:

    • Its read functionality cannot perform stale reads, because all reads happen at the single point in time of the transaction.
    • It cannot perform sub-transactions via performReadWriteTransaction or performReadOnlyTransaction
    • It cannot perform any write operations.

    Because read-only transactions are non-locking and can be performed on points in time in the past, these are recommended for functions that do not perform write operations.

    Declarative Transactions with @Transactional Annotation

    This feature requires a bean of SpannerTransactionManager, which is provided when using spring-cloud-gcp-starter-data-spanner.

    SpannerTemplate and SpannerRepository support running methods with the @Transactional [annotation](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative) as transactions. +If a method annotated with @Transactional calls another method also annotated, then both methods will work within the same transaction. +performReadOnlyTransaction and performReadWriteTransaction cannot be used in @Transactional annotated methods because Cloud Spanner does not support transactions within transactions.

    163.3.7 DML Statements

    SpannerTemplate supports [DML](https://cloud.google.com/spanner/docs/dml-tasks) Statements. +DML statements can be executed in transactions via performReadWriteTransaction or using the @Transactional annotation.

    When DML statements are executed outside of transactions, they are executed in [partitioned-mode](https://cloud.google.com/spanner/docs/dml-tasks#partitioned-dml).

    163.4 Repositories

    Spring Data Repositories are a powerful abstraction that can save you a lot of boilerplate code.

    For example:

    public interface TraderRepository extends SpannerRepository<Trader, String> {
    +}

    Spring Data generates a working implementation of the specified interface, which can be conveniently autowired into an application.

    The Trader type parameter to SpannerRepository refers to the underlying domain type. +The second type parameter, String in this case, refers to the type of the key of the domain type.

    For POJOs with a composite primary key, this ID type parameter can be any descendant of Object[] compatible with all primary key properties, any descendant of Iterable, or com.google.cloud.spanner.Key. +If the domain POJO type only has a single primary key column, then the primary key property type can be used or the Key type.

    For example in case of Trades, that belong to a Trader, TradeRepository would look like this:

    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +
    +}
    public class MyApplication {
    +
    +	@Autowired
    +	SpannerTemplate spannerTemplate;
    +
    +	@Autowired
    +	StudentRepository studentRepository;
    +
    +	public void demo() {
    +
    +		this.tradeRepository.deleteAll();
    +		String traderId = "demo_trader";
    +		Trade t = new Trade();
    +		t.symbol = stock;
    +		t.action = action;
    +		t.traderId = traderId;
    +		t.price = 100.0;
    +		t.shares = 12345.6;
    +		this.spannerTemplate.insert(t);
    +
    +		Iterable<Trade> allTrades = this.tradeRepository.findAll();
    +
    +		int count = this.tradeRepository.countByAction("BUY");
    +
    +	}
    +}

    163.4.1 CRUD Repository

    CrudRepository methods work as expected, with one thing Spanner specific: the save and saveAll methods work as update-or-insert.

    163.4.2 Paging and Sorting Repository

    You can also use PagingAndSortingRepository with Spanner Spring Data. +The sorting and pageable findAll methods available from this interface operate on the current state of the Spanner database. +As a result, beware that the state of the database (and the results) might change when moving page to page.

    163.4.3 Spanner Repository

    The SpannerRepository extends the PagingAndSortingRepository, but adds the read-only and the read-write transaction functionality provided by Spanner. +These transactions work very similarly to those of SpannerOperations, but is specific to the repository’s domain type and provides repository functions instead of template functions.

    For example, this is a read-write transaction:

    @Autowired
    +SpannerRepository myRepo;
    +
    +public String doWorkInsideTransaction() {
    +  return myRepo.performReadOnlyTransaction(
    +    transactionSpannerRepo -> {
    +      // Work with the single-transaction transactionSpannerRepo here.
    +      // This is a SpannerRepository object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    When creating custom repositories for your own domain types and query methods, you can extend SpannerRepository to access Cloud Spanner-specific features as well as all features from PagingAndSortingRepository and CrudRepository.

    163.5 Query Methods

    SpannerRepository supports Query Methods. +Described in the following sections, these are methods residing in your custom repository interfaces of which implementations are generated based on their names and annotations. +Query Methods can read, write, and delete entities in Cloud Spanner. +Parameters to these methods can be any Cloud Spanner data type supported directly or via custom configured converters. +Parameters can also be of type Struct or POJOs. +If a POJO is given as a parameter, it will be converted to a Struct with the same type-conversion logic as used to create write mutations. +Comparisons using Struct parameters are limited to what is available with Cloud Spanner.

    163.5.1 Query methods by convention

    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +    List<Trade> findByAction(String action);
    +
    +	int countByAction(String action);
    +
    +	// Named methods are powerful, but can get unwieldy
    +	List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(
    +  			String action, String symbol, String traderId);
    +}

    In the example above, the query methods in TradeRepository are generated based on the name of the methods, using the Spring Data Query creation naming convention.

    List<Trade> findByAction(String action) would translate to a SELECT * FROM trades WHERE action = ?.

    The function List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(String action, String symbol, String traderId); will be translated as the equivalent of this SQL query:

    SELECT DISTINCT * FROM trades
    +WHERE ACTION = ? AND LOWER(SYMBOL) = LOWER(?) AND TRADER_ID = ?
    +ORDER BY SYMBOL DESC
    +LIMIT 3

    The following filter options are supported:

    • Equality
    • Greater than or equals
    • Greater than
    • Less than or equals
    • Less than
    • Is null
    • Is not null
    • Is true
    • Is false
    • Like a string
    • Not like a string
    • Contains a string
    • Not contains a string

    Note that the phrase SymbolIgnoreCase is translated to LOWER(SYMBOL) = LOWER(?) indicating a non-case-sensitive matching. +The IgnoreCase phrase may only be appended to fields that correspond to columns of type STRING or BYTES. +The Spring Data "AllIgnoreCase" phrase appended at the end of the method name is not supported.

    The Like or NotLike naming conventions:

    List<Trade> findBySymbolLike(String symbolFragment);

    The param symbolFragment can contain wildcard characters for string matching such as _ and %.

    The Contains and NotContains naming conventions:

    List<Trade> findBySymbolContains(String symbolFragment);

    The param symbolFragment is a regular expression that is checked for occurrences.

    Delete queries are also supported. +For example, query methods such as deleteByAction or removeByAction delete entities found by findByAction. +The delete operation happens in a single transaction.

    Delete queries can have the following return types: +* An integer type that is the number of entities deleted +* A collection of entities that were deleted +* void

    163.5.2 Custom SQL/DML query methods

    The example above for List<Trade> fetchByActionNamedQuery(String action) does not match the Spring Data Query creation naming convention, so we have to map a parametrized Spanner SQL query to it.

    The SQL query for the method can be mapped to repository methods in one of two ways:

    • namedQueries properties file
    • using the @Query annotation

    The names of the tags of the SQL correspond to the @Param annotated names of the method parameters.

    Custom SQL query methods can accept a single Sort or Pageable parameter that is applied on top of any sorting or paging in the SQL:

    	@Query("SELECT * FROM trades ORDER BY action DESC")
    +	List<Trade> sortedTrades(Pageable pageable);
    +
    +	@Query("SELECT * FROM trades ORDER BY action DESC LIMIT 1")
    + 	Trade sortedTopTrade(Pageable pageable);

    This can be used:

    	List<Trade> customSortedTrades = tradeRepository.sortedTrades(PageRequest
    +  				.of(2, 2, org.springframework.data.domain.Sort.by(Order.asc("id"))));

    The results would be sorted by "id" in ascending order.

    Your query method can also return non-entity types:

      	@Query("SELECT COUNT(1) FROM trades WHERE action = @action")
    +  	int countByActionQuery(String action);
    +
    +  	@Query("SELECT EXISTS(SELECT COUNT(1) FROM trades WHERE action = @action)")
    +  	boolean existsByActionQuery(String action);
    +
    +  	@Query("SELECT action FROM trades WHERE action = @action LIMIT 1")
    +  	String getFirstString(@Param("action") String action);
    +
    +  	@Query("SELECT action FROM trades WHERE action = @action")
    +  	List<String> getFirstStringList(@Param("action") String action);

    DML statements can also be executed by query methods, but the only possible return value is a long representing the number of affected rows. +The dmlStatement boolean setting must be set on @Query to indicate that the query method is executed as a DML statement.

      	@Query(value = "DELETE FROM trades WHERE action = @action", dmlStatement = true)
    +  	long deleteByActionQuery(String action);

    Query methods with named queries properties

    By default, the namedQueriesLocation attribute on @EnableSpannerRepositories points to the META-INF/spanner-named-queries.properties file. +You can specify the query for a method in the properties file by providing the SQL as the value for the "interface.method" property:

    Trade.fetchByActionNamedQuery=SELECT * FROM trades WHERE trades.action = @tag0
    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +	// This method uses the query from the properties file instead of one generated based on name.
    +	List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
    +}

    Query methods with annotation

    Using the @Query annotation:

    public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +    @Query("SELECT * FROM trades WHERE trades.action = @tag0")
    +    List<Trade> fetchByActionNamedQuery(@Param("tag0") String action);
    +}

    Table names can be used directly. +For example, "trades" in the above example. +Alternatively, table names can be resolved from the @Table annotation on domain classes as well. +In this case, the query should refer to table names with fully qualified class names between : +characters: :fully.qualified.ClassName:. +A full example would look like:

    @Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0")
    +List<Trade> fetchByActionNamedQuery(String action);

    This allows table names evaluated with SpEL to be used in custom queries.

    SpEL can also be used to provide SQL parameters:

    @Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0
    +  AND price > #{#priceRadius * -1} AND price < #{#priceRadius * 2}")
    +List<Trade> fetchByActionNamedQuery(String action, Double priceRadius);

    163.5.3 Projections

    Spring Data Spanner supports projections. +You can define projection interfaces based on domain types and add query methods that return them in your repository:

    public interface TradeProjection {
    +
    +	String getAction();
    +
    +	@Value("#{target.symbol + ' ' + target.action}")
    +	String getSymbolAndAction();
    +}
    +
    +public interface TradeRepository extends SpannerRepository<Trade, Key> {
    +
    +	List<Trade> findByTraderId(String traderId);
    +
    +	List<TradeProjection> findByAction(String action);
    +
    +	@Query("SELECT action, symbol FROM trades WHERE action = @action")
    +	List<TradeProjection> findByQuery(String action);
    +}

    Projections can be provided by name-convention-based query methods as well as by custom SQL queries. +If using custom SQL queries, you can further restrict the columns retrieved from Spanner to just those required by the projection to improve performance.

    Properties of projection types defined using SpEL use the fixed name target for the underlying domain object. +As a result accessing underlying properties take the form target.<property-name>.

    163.5.4 REST Repositories

    When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file:

    <dependency>
    +  <groupId>org.springframework.boot</groupId>
    +  <artifactId>spring-boot-starter-data-rest</artifactId>
    +</dependency>

    If you prefer to configure parameters (such as path), you can use @RepositoryRestResource annotation:

    @RepositoryRestResource(collectionResourceRel = "trades", path = "trades")
    +public interface TradeRepository extends SpannerRepository<Trade, String[]> {
    +}

    For example, you can retrieve all Trade objects in the repository by using curl http://<server>:<port>/trades, or any specific trade via curl http://<server>:<port>/trades/<trader_id>,<trade_id>.

    The separator between your primary key components, id and trader_id in this case, is a comma by default, but can be configured to any string not found in your key values by extending the SpannerKeyIdConverter class:

    @Component
    +class MySpecialIdConverter extends SpannerKeyIdConverter {
    +
    +    @Override
    +    protected String getUrlIdSeparator() {
    +        return ":";
    +    }
    +}

    You can also write trades using curl -XPOST -H"Content-Type: application/json" -d@test.json http://<server>:<port>/trades/ where the file test.json holds the JSON representation of a Trade object.

    163.6 Database and Schema Admin

    Databases and tables inside Spanner instances can be created automatically from SpannerPersistentEntity objects:

    @Autowired
    +private SpannerSchemaUtils spannerSchemaUtils;
    +
    +@Autowired
    +private SpannerDatabaseAdminTemplate spannerDatabaseAdminTemplate;
    +
    +public void createTable(SpannerPersistentEntity entity) {
    +	if(!spannerDatabaseAdminTemplate.tableExists(entity.tableName()){
    +
    +	  // The boolean parameter indicates that the database will be created if it does not exist.
    +	  spannerDatabaseAdminTemplate.executeDdlStrings(Arrays.asList(
    +            spannerSchemaUtils.getCreateTableDDLString(entity.getType())), true);
    +	}
    +}

    Schemas can be generated for entire object hierarchies with interleaved relationships and composite keys.

    163.7 Sample

    A sample application is available.

    164. Spring Data Cloud Datastore

    Spring Data is an abstraction for storing and retrieving POJOs in numerous storage technologies. +Spring Cloud GCP adds Spring Data support for Google Cloud Datastore.

    Maven coordinates for this module only, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-data-datastore</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-datastore'
    +}

    We provide a Spring Boot Starter for Spring Data Datastore, with which you can use our recommended auto-configuration setup. +To use the starter, see the coordinates below.

    Maven:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-data-datastore</artifactId>
    +</dependency>

    Gradle:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-datastore'
    +}

    This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Datastore libraries as well.

    164.1 Configuration

    To setup Spring Data Cloud Datastore, you have to configure the following:

    • Setup the connection details to Google Cloud Datastore.

    164.1.1 Cloud Datastore settings

    You can the use Spring Boot Starter for Spring Data Datastore to autoconfigure Google Cloud Datastore in your Spring application. +It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project. +The following configuration options are available:

    Name

    Description

    Required

    Default value

    spring.cloud.gcp.datastore.enabled

    Enables the Cloud Datastore client

    No

    true

    spring.cloud.gcp.datastore.project-id

    GCP project ID where the Google Cloud Datastore API is hosted, if different from the one in the Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.datastore.credentials.location

    OAuth2 credentials for authenticating with the +Google Cloud Datastore API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.datastore.credentials.encoded-key

    Base64-encoded OAuth2 credentials for authenticating with the +Google Cloud Datastore API, if different from the ones in the +Spring Cloud GCP Core Module

    No

     

    spring.cloud.gcp.datastore.credentials.scopes

    OAuth2 scope for Spring Cloud GCP +Cloud Datastore credentials

    No

    https://www.googleapis.com/auth/datastore

    spring.cloud.gcp.datastore.namespace

    The Cloud Datastore namespace to use

    No

    the Default namespace of Cloud Datastore in your GCP project

    164.1.2 Repository settings

    Spring Data Repositories can be configured via the @EnableDatastoreRepositories annotation on your main @Configuration class. +With our Spring Boot Starter for Spring Data Cloud Datastore, @EnableDatastoreRepositories is automatically added. +It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by @EnableDatastoreRepositories.

    164.1.3 Autoconfiguration

    Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:

    • an instance of DatastoreTemplate
    • an instance of all user defined repositories extending CrudRepository, PagingAndSortingRepository, and DatastoreRepository (an extension of PagingAndSortingRepository with additional Cloud Datastore features) when repositories are enabled
    • an instance of Datastore from the Google Cloud Java Client for Datastore, for convenience and lower level API access

    164.2 Object Mapping

    Spring Data Cloud Datastore allows you to map domain POJOs to Cloud Datastore kinds and entities via annotations:

    @Entity(name = "traders")
    +public class Trader {
    +
    +	@Id
    +	@Field(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@Transient
    +	Double temporaryNumber;
    +}

    Spring Data Cloud Datastore will ignore any property annotated with @Transient. +These properties will not be written to or read from Cloud Datastore.

    164.2.1 Constructors

    Simple constructors are supported on POJOs. +The constructor arguments can be a subset of the persistent properties. +Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument. +Arguments that are not directly set to properties are not supported.

    @Entity(name = "traders")
    +public class Trader {
    +
    +	@Id
    +	@Field(name = "trader_id")
    +	String traderId;
    +
    +	String firstName;
    +
    +	String lastName;
    +
    +	@Transient
    +	Double temporaryNumber;
    +
    +	public Trader(String traderId, String firstName) {
    +	    this.traderId = traderId;
    +	    this.firstName = firstName;
    +	}
    +}

    164.2.2 Kind

    The @Entity annotation can provide the name of the Cloud Datastore kind that stores instances of the annotated class, one per row.

    164.2.3 Keys

    @Id identifies the property corresponding to the ID value.

    You must annotate one of your POJO’s fields as the ID value, because every entity in Cloud Datastore requires a single ID value:

    @Entity(name = "trades")
    +public class Trade {
    +	@Id
    +	@Field(name = "trade_id")
    +	String tradeId;
    +
    +	@Field(name = "trader_id")
    +	String traderId;
    +
    +	String action;
    +
    +	Double price;
    +
    +	Double shares;
    +
    +	String symbol;
    +}

    Datastore can automatically allocate integer ID values. +If a POJO instance with a Long ID property is written to Cloud Datastore with null as the ID value, then Spring Data Cloud Datastore will obtain a newly allocated ID value from Cloud Datastore and set that in the POJO for saving. +Because primitive long ID properties cannot be null and default to 0, keys will not be allocated.

    164.2.4 Fields

    All accessible properties on POJOs are automatically recognized as a Cloud Datastore field. +Field naming is generated by the PropertyNameFieldNamingStrategy by default defined on the DatastoreMappingContext bean. +The @Field annotation optionally provides a different field name than that of the property.

    164.2.5 Supported Types

    Spring Data Cloud Datastore supports the following types for regular fields and elements of collections:

    TypeStored as

    com.google.cloud.Timestamp

    com.google.cloud.datastore.TimestampValue

    com.google.cloud.datastore.Blob

    com.google.cloud.datastore.BlobValue

    com.google.cloud.datastore.LatLng

    com.google.cloud.datastore.LatLngValue

    java.lang.Boolean, boolean

    com.google.cloud.datastore.BooleanValue

    java.lang.Double, double

    com.google.cloud.datastore.DoubleValue

    java.lang.Long, long

    com.google.cloud.datastore.LongValue

    java.lang.Integer, int

    com.google.cloud.datastore.LongValue

    java.lang.String

    com.google.cloud.datastore.StringValue

    com.google.cloud.datastore.Entity

    com.google.cloud.datastore.EntityValue

    com.google.cloud.datastore.Key

    com.google.cloud.datastore.KeyValue

    byte[]

    com.google.cloud.datastore.BlobValue

    Java enum values

    com.google.cloud.datastore.StringValue

    In addition, all types that can be converted to the ones listed in the table by +org.springframework.core.convert.support.DefaultConversionService are supported.

    164.2.6 Custom types

    Custom converters can be used extending the type support for user defined types.

    1. Converters need to implement the org.springframework.core.convert.converter.Converter interface in both directions.
    2. The user defined type needs to be mapped to one of the basic types supported by Cloud Datastore.
    3. An instance of both Converters (read and write) needs to be passed to the DatastoreCustomConversions constructor, which then has to be made available as a @Bean for DatastoreCustomConversions.

    For example:

    We would like to have a field of type Album on our Singer POJO and want it to be stored as a string property:

    @Entity
    +public class Singer {
    +
    +	@Id
    +	String singerId;
    +
    +	String name;
    +
    +	Album album;
    +}

    Where Album is a simple class:

    public class Album {
    +	String albumName;
    +
    +	LocalDate date;
    +}

    We have to define the two converters:

    	//Converter to write custom Album type
    +	static final Converter<Album, String> ALBUM_STRING_CONVERTER =
    +			new Converter<Album, String>() {
    +				@Override
    +				public String convert(Album album) {
    +					return album.getAlbumName() + " " + album.getDate().format(DateTimeFormatter.ISO_DATE);
    +				}
    +			};
    +
    +	//Converters to read custom Album type
    +	static final Converter<String, Album> STRING_ALBUM_CONVERTER =
    +			new Converter<String, Album>() {
    +				@Override
    +				public Album convert(String s) {
    +					String[] parts = s.split(" ");
    +					return new Album(parts[0], LocalDate.parse(parts[parts.length - 1], DateTimeFormatter.ISO_DATE));
    +				}
    +			};

    That will be configured in our @Configuration file:

    @Configuration
    +public class ConverterConfiguration {
    +	@Bean
    +	public DatastoreCustomConversions datastoreCustomConversions() {
    +		return new DatastoreCustomConversions(
    +				Arrays.asList(
    +						ALBUM_STRING_CONVERTER,
    +						STRING_ALBUM_CONVERTER));
    +	}
    +}

    164.2.7 Collections and arrays

    Arrays and collections (types that implement java.util.Collection) of supported types are supported. +They are stored as com.google.cloud.datastore.ListValue. +Elements are converted to Cloud Datastore supported types individually. byte[] is an exception, it is converted to +com.google.cloud.datastore.Blob.

    164.2.8 Custom Converter for collections

    Users can provide converters from List<?> to the custom collection type. +Only read converter is necessary, the Collection API is used on the write side to convert a collection to the internal list type.

    Collection converters need to implement the org.springframework.core.convert.converter.Converter interface.

    Example:

    Let’s improve the Singer class from the previous example. +Instead of a field of type Album, we would like to have a field of type ImmutableSet<Album>:

    @Entity
    +public class Singer {
    +
    +	@Id
    +	String singerId;
    +
    +	String name;
    +
    +	ImmutableSet<Album> albums;
    +}

    We have to define a read converter only:

    static final Converter<List<?>, ImmutableSet<?>> LIST_IMMUTABLE_SET_CONVERTER =
    +			new Converter<List<?>, ImmutableSet<?>>() {
    +				@Override
    +				public ImmutableSet<?> convert(List<?> source) {
    +					return ImmutableSet.copyOf(source);
    +				}
    +			};

    And add it to the list of custom converters:

    @Configuration
    +public class ConverterConfiguration {
    +	@Bean
    +	public DatastoreCustomConversions datastoreCustomConversions() {
    +		return new DatastoreCustomConversions(
    +				Arrays.asList(
    +						LIST_IMMUTABLE_SET_CONVERTER,
    +
    +						ALBUM_STRING_CONVERTER,
    +						STRING_ALBUM_CONVERTER));
    +	}
    +}

    164.3 Relationships

    There are three ways to represent relationships between entities that are described in this section:

    • Embedded entities stored directly in the field of the containing entity
    • @Descendant annotated properties for one-to-many relationships
    • @Reference annotated properties for general relationships without hierarchy

    164.3.1 Embedded Entities

    Fields whose types are also annotated with @Entity are converted to EntityValue and stored inside the parent entity.

    Here is an example of Cloud Datastore entity containing an embedded entity in JSON:

    {
    +  "name" : "Alexander",
    +  "age" : 47,
    +  "child" : {"name" : "Philip"  }
    +}

    This corresponds to a simple pair of Java entities:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity("parents")
    +public class Parent {
    +  @Id
    +  String name;
    +
    +  Child child;
    +}
    +
    +@Entity
    +public class Child {
    +  String name;
    +}

    Child entities are not stored in their own kind. +They are stored in their entirety in the child field of the parents kind.

    Multiple levels of embedded entities are supported.

    [Note]Note

    Embedded entities don’t need to have @Id field, it is only required for top level entities.

    Example:

    Entities can hold embedded entities that are their own type. +We can store trees in Cloud Datastore using this feature:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded;
    +import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity
    +public class EmbeddableTreeNode {
    +  @Id
    +  long value;
    +
    +  EmbeddableTreeNode left;
    +
    +  EmbeddableTreeNode right;
    +
    +  Map<String, Long> longValues;
    +
    +  Map<String, List<Timestamp>> listTimestamps;
    +
    +  public EmbeddableTreeNode(long value, EmbeddableTreeNode left, EmbeddableTreeNode right) {
    +    this.value = value;
    +    this.left = left;
    +    this.right = right;
    +  }
    +}

    Maps

    Maps will be stored as embedded entities where the key values become the field names in the embedded entity. +The value types in these maps can be any regularly supported property type, and the key values will be converted to String using the configured converters.

    Also, a collection of entities can be embedded; it will be converted to ListValue on write.

    Example:

    Instead of a binary tree from the previous example, we would like to store a general tree +(each node can have an arbitrary number of children) in Cloud Datastore. +To do that, we need to create a field of type List<EmbeddableTreeNode>:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded;
    +import org.springframework.data.annotation.Id;
    +
    +public class EmbeddableTreeNode {
    +  @Id
    +  long value;
    +
    +  List<EmbeddableTreeNode> children;
    +
    +  Map<String, EmbeddableTreeNode> siblingNodes;
    +
    +  Map<String, Set<EmbeddableTreeNode>> subNodeGroups;
    +
    +  public EmbeddableTreeNode(List<EmbeddableTreeNode> children) {
    +    this.children = children;
    +  }
    +}

    Because Maps are stored as entities, they can further hold embedded entities:

    • Singular embedded objects in the value can be stored in the values of embedded Maps.
    • Collections of embedded objects in the value can also be stored as the values of embedded Maps.
    • Maps in the value are further stored as embedded entities with the same rules applied recursively for their values.

    164.3.2 Ancestor-Descendant Relationships

    Parent-child relationships are supported via the @Descendants annotation.

    Unlike embedded children, descendants are fully-formed entities residing in their own kinds. +The parent entity does not have an extra field to hold the descendant entities. +Instead, the relationship is captured in the descendants' keys, which refer to their parent entities:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Descendants;
    +import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity("orders")
    +public class ShoppingOrder {
    +  @Id
    +  long id;
    +
    +  @Descendants
    +  List<Item> items;
    +}
    +
    +@Entity("purchased_item")
    +public class Item {
    +  @Id
    +  Key purchasedItemKey;
    +
    +  String name;
    +
    +  Timestamp timeAddedToOrder;
    +}

    For example, an instance of a GQL key-literal representation for Item would also contain the parent ShoppingOrder ID value:

    Key(orders, '12345', purchased_item, 'eggs')

    The GQL key-literal representation for the parent ShoppingOrder would be:

    Key(orders, '12345')

    The Cloud Datastore entities exist separately in their own kinds.

    The ShoppingOrder:

    {
    +  "id" : 12345
    +}

    The two items inside that order:

    {
    +  "purchasedItemKey" : Key(orders, '12345', purchased_item, 'eggs'),
    +  "name" : "eggs",
    +  "timeAddedToOrder" : "2014-09-27 12:30:00.45-8:00"
    +}
    +
    +{
    +  "purchasedItemKey" : Key(orders, '12345', purchased_item, 'sausage'),
    +  "name" : "sausage",
    +  "timeAddedToOrder" : "2014-09-28 11:30:00.45-9:00"
    +}

    The parent-child relationship structure of objects is stored in Cloud Datastore using Datastore’s ancestor relationships. +Because the relationships are defined by the Ancestor mechanism, there is no extra column needed in either the parent or child entity to store this relationship. +The relationship link is part of the descendant entity’s key value. +These relationships can be many levels deep.

    Properties holding child entities must be collection-like, but they can be any of the supported inter-convertible collection-like types that are supported for regular properties such as List, arrays, Set, etc…​ +Child items must have Key as their ID type because Cloud Datastore stores the ancestor relationship link inside the keys of the children.

    Reading or saving an entity automatically causes all subsequent levels of children under that entity to be read or saved, respectively. +If a new child is created and added to a property annotated @Descendants and the key property is left null, then a new key will be allocated for that child. +The ordering of the retrieved children may not be the same as the ordering in the original property that was saved.

    Child entities cannot be moved from the property of one parent to that of another unless the child’s key property is set to null or a value that contains the new parent as an ancestor. +Since Cloud Datastore entity keys can have multiple parents, it is possible that a child entity appears in the property of multiple parent entities. +Because entity keys are immutable in Cloud Datastore, to change the key of a child you must delete the existing one and re-save it with the new key.

    164.3.3 Key Reference Relationships

    General relationships can be stored using the @Reference annotation.

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Reference;
    +import org.springframework.data.annotation.Id;
    +
    +@Entity
    +public class ShoppingOrder {
    +  @Id
    +  long id;
    +
    +  @Reference
    +  List<Item> items;
    +
    +  @Reference
    +  Item specialSingleItem;
    +}
    +
    +@Entity
    +public class Item {
    +  @Id
    +  Key purchasedItemKey;
    +
    +  String name;
    +
    +  Timestamp timeAddedToOrder;
    +}

    @Reference relationships are between fully-formed entities residing in their own kinds. +The relationship between ShoppingOrder and Item entities are stored as a Key field inside ShoppingOrder, which are resolved to the underlying Java entity type by Spring Data Cloud Datastore:

    {
    +  "id" : 12345,
    +  "specialSingleItem" : Key(item, "milk"),
    +  "items" : [ Key(item, "eggs"), Key(item, "sausage") ]
    +}

    Reference properties can either be singular or collection-like. +These properties correspond to actual columns in the entity and Cloud Datastore Kind that hold the key values of the referenced entities. +The referenced entities are full-fledged entities of other Kinds.

    Similar to the @Descendants relationships, reading or writing an entity will recursively read or write all of the referenced entities at all levels. +If referenced entities have null ID values, then they will be saved as new entities and will have ID values allocated by Cloud Datastore. +There are no requirements for relationships between the key of an entity and the keys that entity holds as references. +The order of collection-like reference properties is not preserved when reading back from Cloud Datastore.

    164.4 Datastore Operations & Template

    DatastoreOperations and its implementation, DatastoreTemplate, provides the Template pattern familiar to Spring developers.

    Using the auto-configuration provided by Spring Boot Starter for Datastore, your Spring application context will contain a fully configured DatastoreTemplate object that you can autowire in your application:

    @SpringBootApplication
    +public class DatastoreTemplateExample {
    +
    +	@Autowired
    +	DatastoreTemplate datastoreTemplate;
    +
    +	public void doSomething() {
    +		this.datastoreTemplate.deleteAll(Trader.class);
    +		//...
    +		Trader t = new Trader();
    +		//...
    +		this.datastoreTemplate.save(t);
    +		//...
    +		List<Trader> traders = datastoreTemplate.findAll(Trader.class);
    +		//...
    +	}
    +}

    The Template API provides convenience methods for:

    • Write operations (saving and deleting)
    • Read-write transactions

    164.4.1 GQL Query

    In addition to retrieving entities by their IDs, you can also submit queries.

      <T> Iterable<T> query(Query<? extends BaseEntity> query, Class<T> entityClass);
    +
    +  <A, T> Iterable<T> query(Query<A> query, Function<A, T> entityFunc);
    +
    +  Iterable<Key> queryKeys(Query<Key> query);

    These methods, respectively, allow querying for: +* entities mapped by a given entity class using all the same mapping and converting features +* arbitrary types produced by a given mapping function +* only the Cloud Datastore keys of the entities found by the query

    164.4.2 Find by ID(s)

    Datstore reading a single entity or multiple entities in a kind.

    Using DatastoreTemplate you can execute reads, for example:

    Trader trader = this.datastoreTemplate.findById("trader1", Trader.class);
    +
    +List<Trader> traders = this.datastoreTemplate.findAllById(ImmutableList.of("trader1", "trader2"), Trader.class);
    +
    +List<Trader> allTraders = this.datastoreTemplate.findAll(Trader.class);

    Cloud Datastore executes key-based reads with strong consistency, but queries with eventual consistency. +In the example above the first two reads utilize keys, while the third is executed using a query based on the corresponding Kind of Trader.

    Indexes

    By default, all fields are indexed. +To disable indexing on a particular field, @Unindexed annotation can be used.

    Example:

    import org.springframework.cloud.gcp.data.datastore.core.mapping.Unindexed;
    +
    +public class ExampleItem {
    +	long indexedField;
    +
    +	@Unindexed
    +	long unindexedField;
    +}

    When using queries directly or via Query Methods, Cloud Datastore requires composite custom indexes if the select statement is not SELECT * or if there is more than one filtering condition in the WHERE clause.

    Read with offsets, limits, and sorting

    DatastoreRepository and custom-defined entity repositories implement the Spring Data PagingAndSortingRepository, which supports offsets and limits using page numbers and page sizes. +Paging and sorting options are also supported in DatastoreTemplate by supplying a DatastoreQueryOptions to findAll.

    Partial read

    This feature is not supported yet.

    164.4.3 Write / Update

    The write methods of DatastoreOperations accept a POJO and writes all of its properties to Datastore. +The required Datastore kind and entity metadata is obtained from the given object’s actual type.

    If a POJO was retrieved from Datastore and its ID value was changed and then written or updated, the operation will occur as if against a row with the new ID value. +The entity with the original ID value will not be affected.

    Trader t = new Trader();
    +this.datastoreTemplate.save(t);

    The save method behaves as update-or-insert.

    Partial Update

    This feature is not supported yet.

    164.4.4 Transactions

    Read and write transactions are provided by DatastoreOperations via the performTransaction method:

    @Autowired
    +DatastoreOperations myDatastoreOperations;
    +
    +public String doWorkInsideTransaction() {
    +  return myDatastoreOperations.performTransaction(
    +    transactionDatastoreOperations -> {
    +      // Work with transactionDatastoreOperations here.
    +      // It is also a DatastoreOperations object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    The performTransaction method accepts a Function that is provided an instance of a DatastoreOperations object. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular DatastoreOperations with an exception:

    • It cannot perform sub-transactions.

    Because of Cloud Datastore’s consistency guarantees, there are limitations to the operations and relationships among entities used inside transactions.

    Declarative Transactions with @Transactional Annotation

    This feature requires a bean of DatastoreTransactionManager, which is provided when using spring-cloud-gcp-starter-data-datastore.

    DatastoreTemplate and DatastoreRepository support running methods with the @Transactional annotation as transactions. +If a method annotated with @Transactional calls another method also annotated, then both methods will work within the same transaction. +performTransaction cannot be used in @Transactional annotated methods because Cloud Datastore does not support transactions within transactions.

    164.4.5 Read-Write Support for Maps

    You can work with Maps of type Map<String, ?> instead of with entity objects by directly reading and writing them to and from Cloud Datastore.

    [Note]Note

    This is a different situation than using entity objects that contain Map properties.

    The map keys are used as field names for a Datastore entity and map values are converted to Datastore supported types. +Only simple types are supported (i.e. collections are not supported). +Converters for custom value types can be added (see Section 163.2.10, “Custom types” section).

    Example:

    Map<String, Long> map = new HashMap<>();
    +map.put("field1", 1L);
    +map.put("field2", 2L);
    +map.put("field3", 3L);
    +
    +keyForMap = datastoreTemplate.createKey("kindName", "id");
    +
    +//write a map
    +datastoreTemplate.writeMap(keyForMap, map);
    +
    +//read a map
    +Map<String, Long> loadedMap = datastoreTemplate.findByIdAsMap(keyForMap, Long.class);

    164.5 Repositories

    Spring Data Repositories are an abstraction that can reduce boilerplate code.

    For example:

    public interface TraderRepository extends DatastoreRepository<Trader, String> {
    +}

    Spring Data generates a working implementation of the specified interface, which can be autowired into an application.

    The Trader type parameter to DatastoreRepository refers to the underlying domain type. +The second type parameter, String in this case, refers to the type of the key of the domain type.

    public class MyApplication {
    +
    +	@Autowired
    +	TraderRepository traderRepository;
    +
    +	public void demo() {
    +
    +		this.traderRepository.deleteAll();
    +		String traderId = "demo_trader";
    +		Trader t = new Trader();
    +		t.traderId = traderId;
    +		this.tradeRepository.save(t);
    +
    +		Iterable<Trader> allTraders = this.traderRepository.findAll();
    +
    +		int count = this.traderRepository.count();
    +	}
    +}

    Repositories allow you to define custom Query Methods (detailed in the following sections) for retrieving, counting, and deleting based on filtering and paging parameters. +Filtering parameters can be of types supported by your configured custom converters.

    164.5.1 Query methods by convention

    public interface TradeRepository extends DatastoreRepository<Trade, String[]> {
    +  List<Trader> findByAction(String action);
    +
    +  int countByAction(String action);
    +
    +  boolean existsByAction(String action);
    +
    +  List<Trade> findTop3ByActionAndSymbolAndPriceGreaterThanAndPriceLessThanOrEqualOrderBySymbolDesc(
    +  			String action, String symbol, double priceFloor, double priceCeiling);
    +
    +  Page<TestEntity> findByAction(String action, Pageable pageable);
    +
    +  Slice<TestEntity> findBySymbol(String symbol, Pageable pageable);
    +
    +  List<TestEntity> findBySymbol(String symbol, Sort sort);
    +}

    In the example above the query methods in TradeRepository are generated based on the name of the methods using thehttps://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation[Spring Data Query creation naming convention].

    Cloud Datastore only supports filter components joined by AND, and the following operations:

    • equals
    • greater than or equals
    • greater than
    • less than or equals
    • less than
    • is null

    After writing a custom repository interface specifying just the signatures of these methods, implementations are generated for you and can be used with an auto-wired instance of the repository. +Because of Cloud Datastore’s requirement that explicitly selected fields must all appear in a composite index together, find name-based query methods are run as SELECT *.

    Delete queries are also supported. +For example, query methods such as deleteByAction or removeByAction delete entities found by findByAction. +Delete queries are executed as separate read and delete operations instead of as a single transaction because Cloud Datastore cannot query in transactions unless ancestors for queries are specified. +As a result, removeBy and deleteBy name-convention query methods cannot be used inside transactions via either performInTransaction or @Transactional annotation.

    Delete queries can have the following return types:

    • An integer type that is the number of entities deleted
    • A collection of entities that were deleted
    • 'void'

    Methods can have org.springframework.data.domain.Pageable parameter to control pagination and sorting, or org.springframework.data.domain.Sort parameter to control sorting only. +See Spring Data documentation for details.

    For returning multiple items in a repository method, we support Java collections as well as org.springframework.data.domain.Page and org.springframework.data.domain.Slice. +If a method’s return type is org.springframework.data.domain.Page, the returned object will include current page, total number of results and total number of pages.

    [Note]Note

    Methods that return Page execute an additional query to compute total number of pages. +Methods that return Slice, on the other hand, don’t execute any additional queries and therefore are much more efficient.

    164.5.2 Custom GQL query methods

    Custom GQL queries can be mapped to repository methods in one of two ways:

    • namedQueries properties file
    • using the @Query annotation

    Query methods with annotation

    Using the @Query annotation:

    The names of the tags of the GQL correspond to the @Param annotated names of the method parameters.

    public interface TraderRepository extends DatastoreRepository<Trader, String> {
    +
    +  @Query("SELECT * FROM traders WHERE name = @trader_name")
    +  List<Trader> tradersByName(@Param("trader_name") String traderName);
    +
    +  @Query("SELECT * FROM  test_entities_ci WHERE id = @id_val")
    +  TestEntity getOneTestEntity(@Param("id_val") long id);
    +}

    The following parameter types are supported:

    • com.google.cloud.Timestamp
    • com.google.cloud.datastore.Blob
    • com.google.cloud.datastore.Key
    • com.google.cloud.datastore.Cursor
    • java.lang.Boolean
    • java.lang.Double
    • java.lang.Long
    • java.lang.String
    • enum values. +These are queried as String values.

    With the exception of Cursor, array forms of each of the types are also supported.

    If you would like to obtain the count of items of a query or if there are any items returned by the query, set the count = true or exists = true properties of the @Query annotation, respectively. +The return type of the query method in these cases should be an integer type or a boolean type.

    Cloud Datastore provides provides the SELECT key FROM …​ special column for all kinds that retrieves the Key`s of each row. +Selecting this special `key column is especially useful and efficient for count and exists queries.

    You can also query for non-entity types:

    	@Query(value = "SELECT __key__ from test_entities_ci")
    +	List<Key> getKeys();
    +
    +	@Query(value = "SELECT __key__ from test_entities_ci limit 1")
    +	Key getKey();
    +
    +	@Query("SELECT id FROM test_entities_ci WHERE id <= @id_val")
    +	List<String> getIds(@Param("id_val") long id);
    +
    +	@Query("SELECT id FROM test_entities_ci WHERE id <= @id_val limit 1")
    +	String getOneId(@Param("id_val") long id);

    SpEL can be used to provide GQL parameters:

    @Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act
    +  AND price > :#{#priceRadius * -1} AND price < :#{#priceRadius * 2}")
    +List<Trade> fetchByActionNamedQuery(@Param("act") String action, @Param("priceRadius") Double r);

    Kind names can be directly written in the GQL annotations. +Kind names can also be resolved from the @Entity annotation on domain classes.

    In this case, the query should refer to table names with fully qualified class names surrounded by | characters: |fully.qualified.ClassName|. +This is useful when SpEL expressions appear in the kind name provided to the @Entity annotation. +For example:

    @Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act")
    +List<Trade> fetchByActionNamedQuery(@Param("act") String action);

    Query methods with named queries properties

    You can also specify queries with Cloud Datastore parameter tags and SpEL expressions in properties files.

    By default, the namedQueriesLocation attribute on @EnableDatastoreRepositories points to the META-INF/datastore-named-queries.properties file. +You can specify the query for a method in the properties file by providing the GQL as the value for the "interface.method" property:

    Trader.fetchByName=SELECT * FROM traders WHERE name = @tag0
    public interface TraderRepository extends DatastoreRepository<Trader, String> {
    +
    +	// This method uses the query from the properties file instead of one generated based on name.
    +	List<Trader> fetchByName(@Param("tag0") String traderName);
    +
    +}

    164.5.3 Transactions

    These transactions work very similarly to those of DatastoreOperations, but is specific to the repository’s domain type and provides repository functions instead of template functions.

    For example, this is a read-write transaction:

    @Autowired
    +DatastoreRepository myRepo;
    +
    +public String doWorkInsideTransaction() {
    +  return myRepo.performTransaction(
    +    transactionDatastoreRepo -> {
    +      // Work with the single-transaction transactionDatastoreRepo here.
    +      // This is a DatastoreRepository object.
    +
    +      return "transaction completed";
    +    }
    +  );
    +}

    164.5.4 Projections

    Spring Data Cloud Datastore supports projections. +You can define projection interfaces based on domain types and add query methods that return them in your repository:

    public interface TradeProjection {
    +
    +	String getAction();
    +
    +	@Value("#{target.symbol + ' ' + target.action}")
    +	String getSymbolAndAction();
    +}
    +
    +public interface TradeRepository extends DatastoreRepository<Trade, Key> {
    +
    +	List<Trade> findByTraderId(String traderId);
    +
    +	List<TradeProjection> findByAction(String action);
    +
    +	@Query("SELECT action, symbol FROM trades WHERE action = @action")
    +	List<TradeProjection> findByQuery(String action);
    +}

    Projections can be provided by name-convention-based query methods as well as by custom GQL queries. +If using custom GQL queries, you can further restrict the fields retrieved from Cloud Datastore to just those required by the projection. +However, custom select statements (those not using SELECT *) require composite indexes containing the selected fields.

    Properties of projection types defined using SpEL use the fixed name target for the underlying domain object. +As a result, accessing underlying properties take the form target.<property-name>.

    164.5.5 REST Repositories

    When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file:

    <dependency>
    +  <groupId>org.springframework.boot</groupId>
    +  <artifactId>spring-boot-starter-data-rest</artifactId>
    +</dependency>

    If you prefer to configure parameters (such as path), you can use @RepositoryRestResource annotation:

    @RepositoryRestResource(collectionResourceRel = "trades", path = "trades")
    +public interface TradeRepository extends DatastoreRepository<Trade, String[]> {
    +}

    For example, you can retrieve all Trade objects in the repository by using curl http://<server>:<port>/trades, or any specific trade via curl http://<server>:<port>/trades/<trader_id>.

    You can also write trades using curl -XPOST -H"Content-Type: application/json" -d@test.json http://<server>:<port>/trades/ where the file test.json holds the JSON representation of a Trade object.

    To delete trades, you can use curl -XDELETE http://<server>:<port>/trades/<trader_id>

    164.6 Sample

    A Simple Spring Boot Application and more advanced Sample Spring Boot Application are provided to show how to use the Spring Data Cloud Datastore starter and template.

    165. Cloud Memorystore for Redis

    165.1 Spring Caching

    Cloud Memorystore for Redis provides a fully managed in-memory data store service. +Cloud Memorystore is compatible with the Redis protocol, allowing easy integration with Spring Caching.

    All you have to do is create a Cloud Memorystore instance and use its IP address in application.properties file as spring.redis.host property value. +Everything else is exactly the same as setting up redis-backed Spring caching.

    [Note]Note

    Memorystore instances and your application instances have to be located in the same region.

    In short, the following dependencies are needed:

    <dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-cache</artifactId>
    +</dependency>
    +<dependency>
    +    <groupId>org.springframework.boot</groupId>
    +    <artifactId>spring-boot-starter-data-redis</artifactId>
    +</dependency>

    And then you can use org.springframework.cache.annotation.Cacheable annotation for methods you’d like to be cached.

    @Cacheable("cache1")
    +public String hello(@PathVariable String name) {
    +    ....
    +}

    If you are interested in a detailed how-to guide, please check Spring Boot Caching using Cloud Memorystore codelab.

    Cloud Memorystore documentation can be found here.

    166. Cloud Identity-Aware Proxy (IAP) Authentication

    Cloud Identity-Aware Proxy (IAP) provides a security layer over applications deployed to Google Cloud.

    The IAP starter uses Spring Security OAuth 2.0 Resource Server functionality to automatically extract user identity from the proxy-injected x-goog-iap-jwt-assertion HTTP header.

    The following claims are validated automatically:

    • Issue time
    • Expiration time
    • Issuer
    • Audience

    The audience ("aud") validation is automatically configured when the application is running on App Engine Standard or App Engine Flexible. +For other runtime environments, a custom audience must be provided through spring.cloud.gcp.security.iap.audience property. +The custom property, if specified, overrides the automatic App Engine audience detection.

    [Important]Important

    There is no automatic audience string configuration for Compute Engine or Kubernetes Engine. +To use the IAP starter on GCE/GKE, find the Audience string per instructions in the Verify the JWT payload guide, and specify it in the spring.cloud.gcp.security.iap.audience property. +Otherwise, the application will fail to start with No qualifying bean of type 'org.springframework.cloud.gcp.security.iap.AudienceProvider' available message.

    [Note]Note

    If you create a custom WebSecurityConfigurerAdapter, enable extracting user identity by adding .oauth2ResourceServer().jwt() configuration to the HttpSecurity object. + If no custom WebSecurityConfigurerAdapter is present, nothing needs to be done because Spring Boot will add this customization by default.

    Starter Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +    <groupId>org.springframework.cloud</groupId>
    +    <artifactId>spring-cloud-gcp-starter-security-iap</artifactId>
    +</dependency>

    Starter Gradle coordinates:

    dependencies {
    +    compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-security-iap'
    +}

    166.1 Configuration

    The following properties are available.

    [Caution]Caution

    Modifying registry, algorithm, and header properties might be useful for testing, but the defaults should not be changed in production.

    NameDescriptionRequiredDefault

    spring.cloud.gcp.security.iap.registry

    Link to JWK public key registry.

    true

    https://www.gstatic.com/iap/verify/public_key-jwk

    spring.cloud.gcp.security.iap.algorithm

    Encryption algorithm used to sign the JWK token.

    true

    ES256

    spring.cloud.gcp.security.iap.header

    Header from which to extract the JWK key.

    true

    x-goog-iap-jwt-assertion

    spring.cloud.gcp.security.iap.issuer

    JWK issuer to verify.

    true

    https://cloud.google.com/iap

    spring.cloud.gcp.security.iap.audience

    Custom JWK audience to verify.

    false on App Engine; true on GCE/GKE

     

    166.2 Sample

    A sample application is available.

    167. Google Cloud Vision

    The Google Cloud Vision API allows users to leverage machine learning algorithms for processing images including: image classification, face detection, text extraction, and others.

    Spring Cloud GCP provides:

    • A convenience starter which automatically configures authentication settings and client objects needed to begin using the Google Cloud Vision API.
    • A Cloud Vision Template which simplifies interactions with the Cloud Vision API.

      • Allows you to easily send images to the API as Spring Resources.
      • Offers convenience methods for common operations, such as extracting the text from an image.

    Maven coordinates, using Spring Cloud GCP BOM:

    <dependency>
    +  <groupId>org.springframework.cloud</groupId>
    +  <artifactId>spring-cloud-gcp-starter-vision</artifactId>
    +</dependency>

    Gradle coordinates:

    dependencies {
    +  compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-vision'
    +}

    167.1 Cloud Vision Template

    The CloudVisionTemplate offers a simple way to use the Cloud Vision APIs with Spring Resources.

    After you add the spring-cloud-gcp-starter-vision dependency to your project, you may @Autowire an instance of CloudVisionTemplate to use in your code.

    The CloudVisionTemplate offers the following method for interfacing with Cloud Vision:

    public AnnotateImageResponse analyzeImage(Resource imageResource, Feature.Type…​ featureTypes)

    Parameters:

    • Resource imageResource refers to the Spring Resource of the image object you wish to analyze. +The Google Cloud Vision documentation provides a list of the image types that they support.
    • Feature.Type…​ featureTypes refers to a var-arg array of Cloud Vision Features to extract from the image. +A feature refers to a kind of image analysis one wishes to perform on an image, such as label detection, OCR recognition, facial detection, etc. +One may specify multiple features to analyze within one request. +A full list of Cloud Vision Features is provided in the Cloud Vision Feature docs.

    Returns:

    • AnnotateImageResponse contains the results of all the feature analyses that were specified in the request. +For each feature type that you provide in the request, AnnotateImageResponse provides a getter method to get the result of that feature analysis. +For example, if you analyzed an image using the LABEL_DETECTION feature, you would retrieve the results from the response using annotateImageResponse.getLabelAnnotationsList().

      AnnotateImageResponse is provided by the Google Cloud Vision libraries; please consult the RPC reference or Javadoc for more details. +Additionally, you may consult the Cloud Vision docs to familiarize yourself with the concepts and features of the API.

    167.2 Detect Image Labels Example

    Image labeling refers to producing labels that describe the contents of an image. +Below is a code sample of how this is done using the Cloud Vision Spring Template.

    @Autowired
    +private ResourceLoader resourceLoader;
    +
    +@Autowired
    +private CloudVisionTemplate cloudVisionTemplate;
    +
    +public void processImage() {
    +  Resource imageResource = this.resourceLoader.getResource("my_image.jpg");
    +  AnnotateImageResponse response = this.cloudVisionTemplate.analyzeImage(
    +      imageResource, Type.LABEL_DETECTION);
    +  System.out.println("Image Classification results: " + response.getLabelAnnotationsList());
    +}

    167.3 Sample

    A Sample Spring Boot Application is provided to show how to use the Cloud Vision starter and template.

    168. Cloud Foundry

    Spring Cloud GCP provides support for Cloud Foundry’s GCP Service Broker. +Our Pub/Sub, Cloud Spanner, Storage, Stackdriver Trace and Cloud SQL MySQL and PostgreSQL starters are Cloud Foundry aware and retrieve properties like project ID, credentials, etc., that are used in auto configuration from the Cloud Foundry environment.

    In cases like Pub/Sub’s topic and subscription, or Storage’s bucket name, where those parameters are not used in auto configuration, you can fetch them using the VCAP mapping provided by Spring Boot. +For example, to retrieve the provisioned Pub/Sub topic, you can use the vcap.services.mypubsub.credentials.topic_name property from the application environment.

    [Note]Note

    If the same service is bound to the same application more than once, the auto configuration will not be able to choose among bindings and will not be activated for that service. +This includes both MySQL and PostgreSQL bindings to the same app.

    [Warning]Warning

    In order for the Cloud SQL integration to work in Cloud Foundry, auto-reconfiguration must be disabled. +You can do so using the cf set-env <APP> JBP_CONFIG_SPRING_AUTO_RECONFIGURATION '{enabled: false}' command. +Otherwise, Cloud Foundry will produce a DataSource with an invalid JDBC URL (i.e., jdbc:mysql://null/null).

    169. Kotlin Support

    The latest version of the Spring Framework provides first-class support for Kotlin. +For Kotlin users of Spring, the Spring Cloud GCP libraries work out-of-the-box and are fully interoperable with Kotlin applications.

    For more information on building a Spring application in Kotlin, please consult the Spring Kotlin documentation.

    169.1 Prerequisites

    Ensure that your Kotlin application is properly set up. +Based on your build system, you will need to include the correct Kotlin build plugin in your project:

    Depending on your application’s needs, you may need to augment your build configuration with compiler plugins:

    Once your Kotlin project is properly configured, the Spring Cloud GCP libraries will work within your application without any additional setup.

    170. Sample

    A Kotlin sample application is provided to demonstrate a working Maven setup and various Spring Cloud GCP integrations from within Kotlin.

    Part XIX. Appendix: Compendium of Configuration Properties

    Name

    Default

    Description

    aws.paramstore.default-context

    application

     

    aws.paramstore.enabled

    true

    Is AWS Parameter Store support enabled.

    aws.paramstore.fail-fast

    true

    Throw exceptions during config lookup if true, otherwise, log warnings.

    aws.paramstore.name

     

    Alternative to spring.application.name to use in looking up values in AWS Parameter Store.

    aws.paramstore.prefix

    /config

    Prefix indicating first level for every property. Value must start with a forward slash followed by a valid path segment or be empty. Defaults to "/config".

    aws.paramstore.profile-separator

    _

     

    cloud.aws.credentials.access-key

     

    The access key to be used with a static provider.

    cloud.aws.credentials.instance-profile

    true

    Configures an instance profile credentials provider with no further configuration.

    cloud.aws.credentials.profile-name

     

    The AWS profile name.

    cloud.aws.credentials.profile-path

     

    The AWS profile path.

    cloud.aws.credentials.secret-key

     

    The secret key to be used with a static provider.

    cloud.aws.credentials.use-default-aws-credentials-chain

    false

    Use the DefaultAWSCredentials Chain instead of configuring a custom credentials chain.

    cloud.aws.loader.core-pool-size

    1

    The core pool size of the Task Executor used for parallel S3 interaction. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setCorePoolSize(int)

    cloud.aws.loader.max-pool-size

     

    The maximum pool size of the Task Executor used for parallel S3 interaction. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize(int)

    cloud.aws.loader.queue-capacity

     

    The maximum queue capacity for backed up S3 requests. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setQueueCapacity(int)

    cloud.aws.region.auto

    true

    Enables automatic region detection based on the EC2 meta data service.

    cloud.aws.region.static

      

    cloud.aws.stack.auto

    true

    Enables the automatic stack name detection for the application.

    cloud.aws.stack.name

    myStackName

    The name of the manually configured stack name that will be used to retrieve the resources.

    encrypt.fail-on-error

    true

    Flag to say that a process should fail if there is an encryption or decryption error.

    encrypt.key

     

    A symmetric key. As a stronger alternative, consider using a keystore.

    encrypt.key-store.alias

     

    Alias for a key in the store.

    encrypt.key-store.location

     

    Location of the key store file, e.g. classpath:/keystore.jks.

    encrypt.key-store.password

     

    Password that locks the keystore.

    encrypt.key-store.secret

     

    Secret protecting the key (defaults to the same as the password).

    encrypt.key-store.type

    jks

    The KeyStore type. Defaults to jks.

    encrypt.rsa.algorithm

     

    The RSA algorithm to use (DEFAULT or OEAP). Once it is set, do not change it (or existing ciphers will not be decryptable).

    encrypt.rsa.salt

    deadbeef

    Salt for the random secret used to encrypt cipher text. Once it is set, do not change it (or existing ciphers will not be decryptable).

    encrypt.rsa.strong

    false

    Flag to indicate that "strong" AES encryption should be used internally. If true, then the GCM algorithm is applied to the AES encrypted bytes. Default is false (in which case "standard" CBC is used instead). Once it is set, do not change it (or existing ciphers will not be decryptable).

    encrypt.salt

    deadbeef

    A salt for the symmetric key, in the form of a hex-encoded byte array. As a stronger alternative, consider using a keystore.

    endpoints.zookeeper.enabled

    true

    Enable the /zookeeper endpoint to inspect the state of zookeeper.

    eureka.client.healthcheck.enabled

    true

    Enables the Eureka health check handler.

    health.config.enabled

    false

    Flag to indicate that the config server health indicator should be installed.

    health.config.time-to-live

    0

    Time to live for cached result, in milliseconds. Default 300000 (5 min).

    hystrix.metrics.enabled

    true

    Enable Hystrix metrics polling. Defaults to true.

    hystrix.metrics.polling-interval-ms

    2000

    Interval between subsequent polling of metrics. Defaults to 2000 ms.

    hystrix.shareSecurityContext

    false

    Enables auto-configuration of the Hystrix concurrency strategy plugin hook who will transfer the SecurityContext from your main thread to the one used by the Hystrix command.

    management.endpoint.bindings.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.bindings.enabled

    true

    Whether to enable the bindings endpoint.

    management.endpoint.bus-env.enabled

    true

    Whether to enable the bus-env endpoint.

    management.endpoint.bus-refresh.enabled

    true

    Whether to enable the bus-refresh endpoint.

    management.endpoint.channels.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.channels.enabled

    true

    Whether to enable the channels endpoint.

    management.endpoint.consul.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.consul.enabled

    true

    Whether to enable the consul endpoint.

    management.endpoint.env.post.enabled

    true

    Enables writable environment endpoint.

    management.endpoint.features.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.features.enabled

    true

    Whether to enable the features endpoint.

    management.endpoint.gateway.enabled

    true

    Whether to enable the gateway endpoint.

    management.endpoint.hystrix.config

     

    Hystrix settings. These are traditionally set using servlet parameters. Refer to the documentation of Hystrix for more details.

    management.endpoint.hystrix.stream.enabled

    true

    Whether to enable the hystrix.stream endpoint.

    management.endpoint.pause.enabled

    true

    Enable the /pause endpoint (to send Lifecycle.stop()).

    management.endpoint.refresh.enabled

    true

    Enable the /refresh endpoint to refresh configuration and re-initialize refresh scoped beans.

    management.endpoint.restart.enabled

    true

    Enable the /restart endpoint to restart the application context.

    management.endpoint.resume.enabled

    true

    Enable the /resume endpoint (to send Lifecycle.start()).

    management.endpoint.service-registry.cache.time-to-live

    0ms

    Maximum time that a response can be cached.

    management.endpoint.service-registry.enabled

    true

    Whether to enable the service-registry endpoint.

    management.health.binders.enabled

    true

    Allows to enable/disable binder’s' health indicators. If you want to disable health indicator completely, then set it to false.

    management.health.refresh.enabled

    true

    Enable the health endpoint for the refresh scope.

    management.health.zookeeper.enabled

    true

    Enable the health endpoint for zookeeper.

    management.metrics.binders.hystrix.enabled

    true

    Enables creation of OK Http Client factory beans.

    management.metrics.export.cloudwatch.batch-size

      

    management.metrics.export.cloudwatch.connect-timeout

      

    management.metrics.export.cloudwatch.enabled

    true

    Enables cloud watch metrics.

    management.metrics.export.cloudwatch.namespace

     

    Cloud watch namespace.

    management.metrics.export.cloudwatch.num-threads

      

    management.metrics.export.cloudwatch.read-timeout

      

    management.metrics.export.cloudwatch.step

      

    maven.checksum-policy

      

    maven.connect-timeout

      

    maven.enable-repository-listener

      

    maven.local-repository

      

    maven.offline

      

    maven.proxy

      

    maven.remote-repositories

      

    maven.request-timeout

      

    maven.resolve-pom

      

    maven.update-policy

      

    proxy.auth.load-balanced

    false

     

    proxy.auth.routes

     

    Authentication strategy per route.

    ribbon.eager-load.clients

      

    ribbon.eager-load.enabled

    false

     

    ribbon.http.client.enabled

    false

    Deprecated property to enable Ribbon RestClient.

    ribbon.okhttp.enabled

    false

    Enables the use of the OK HTTP Client with Ribbon.

    ribbon.restclient.enabled

    false

    Enables the use of the deprecated Ribbon RestClient.

    ribbon.secure-ports

      

    spring.cloud.bus.ack.destination-service

     

    Service that wants to listen to acks. By default null (meaning all services).

    spring.cloud.bus.ack.enabled

    true

    Flag to switch off acks (default on).

    spring.cloud.bus.destination

    springCloudBus

    Name of Spring Cloud Stream destination for messages.

    spring.cloud.bus.enabled

    true

    Flag to indicate that the bus is enabled.

    spring.cloud.bus.env.enabled

    true

    Flag to switch off environment change events (default on).

    spring.cloud.bus.id

    application

    The identifier for this application instance.

    spring.cloud.bus.refresh.enabled

    true

    Flag to switch off refresh events (default on).

    spring.cloud.bus.trace.enabled

    false

    Flag to switch on tracing of acks (default off).

    spring.cloud.cloudfoundry.discovery.default-server-port

    80

    Port to use when no port is defined by ribbon.

    spring.cloud.cloudfoundry.discovery.enabled

    true

    Flag to indicate that discovery is enabled.

    spring.cloud.cloudfoundry.discovery.heartbeat-frequency

    5000

    Frequency in milliseconds of poll for heart beat. The client will poll on this frequency and broadcast a list of service ids.

    spring.cloud.cloudfoundry.discovery.order

    0

    Order of the discovery client used by CompositeDiscoveryClient for sorting available clients.

    spring.cloud.cloudfoundry.org

     

    Organization name to initially target.

    spring.cloud.cloudfoundry.password

     

    Password for user to authenticate and obtain token.

    spring.cloud.cloudfoundry.skip-ssl-validation

    false

     

    spring.cloud.cloudfoundry.space

     

    Space name to initially target.

    spring.cloud.cloudfoundry.url

     

    URL of Cloud Foundry API (Cloud Controller).

    spring.cloud.cloudfoundry.username

     

    Username to authenticate (usually an email address).

    spring.cloud.compatibility-verifier.compatible-boot-versions

    2.1.x

    Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don’t want to specify a concrete value. Example: {@code 3.4.x}

    spring.cloud.compatibility-verifier.enabled

    false

    Enables creation of Spring Cloud compatibility verification.

    spring.cloud.config.allow-override

    true

    Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true.

    spring.cloud.config.discovery.enabled

    false

    Flag to indicate that config server discovery is enabled (config server URL will be looked up via discovery).

    spring.cloud.config.discovery.service-id

    configserver

    Service id to locate config server.

    spring.cloud.config.enabled

    true

    Flag to say that remote configuration is enabled. Default true;

    spring.cloud.config.fail-fast

    false

    Flag to indicate that failure to connect to the server is fatal (default false).

    spring.cloud.config.headers

     

    Additional headers used to create the client request.

    spring.cloud.config.label

     

    The label name to use to pull remote configuration properties. The default is set on the server (generally "master" for a git based server).

    spring.cloud.config.name

     

    Name of application used to fetch remote properties.

    spring.cloud.config.override-none

    false

    Flag to indicate that when {@link #setAllowOverride(boolean) allowOverride} is true, external properties should take lowest priority and should not override any existing property sources (including local config files). Default false.

    spring.cloud.config.override-system-properties

    true

    Flag to indicate that the external properties should override system properties. Default true.

    spring.cloud.config.password

     

    The password to use (HTTP Basic) when contacting the remote server.

    spring.cloud.config.profile

    default

    The default profile to use when fetching remote configuration (comma-separated). Default is "default".

    spring.cloud.config.request-connect-timeout

    0

    timeout on waiting to connect to the Config Server.

    spring.cloud.config.request-read-timeout

    0

    timeout on waiting to read data from the Config Server.

    spring.cloud.config.retry.initial-interval

    1000

    Initial retry interval in milliseconds.

    spring.cloud.config.retry.max-attempts

    6

    Maximum number of attempts.

    spring.cloud.config.retry.max-interval

    2000

    Maximum interval for backoff.

    spring.cloud.config.retry.multiplier

    1.1

    Multiplier for next interval.

    spring.cloud.config.send-state

    true

    Flag to indicate whether to send state. Default true.

    spring.cloud.config.server.accept-empty

    true

    Flag to indicate that If HTTP 404 needs to be sent if Application is not Found.

    spring.cloud.config.server.bootstrap

    false

    Flag indicating that the config server should initialize its own Environment with properties from the remote repository. Off by default because it delays startup but can be useful when embedding the server in another application.

    spring.cloud.config.server.credhub.ca-cert-files

      

    spring.cloud.config.server.credhub.connection-timeout

      

    spring.cloud.config.server.credhub.oauth2.registration-id

      

    spring.cloud.config.server.credhub.order

      

    spring.cloud.config.server.credhub.read-timeout

      

    spring.cloud.config.server.credhub.url

      

    spring.cloud.config.server.default-application-name

    application

    Default application name when incoming requests do not have a specific one.

    spring.cloud.config.server.default-label

     

    Default repository label when incoming requests do not have a specific label.

    spring.cloud.config.server.default-profile

    default

    Default application profile when incoming requests do not have a specific one.

    spring.cloud.config.server.encrypt.enabled

    true

    Enable decryption of environment properties before sending to client.

    spring.cloud.config.server.git.basedir

     

    Base directory for local working copy of repository.

    spring.cloud.config.server.git.clone-on-start

    false

    Flag to indicate that the repository should be cloned on startup (not on demand). Generally leads to slower startup but faster first query.

    spring.cloud.config.server.git.default-label

     

    The default label to be used with the remote repository.

    spring.cloud.config.server.git.delete-untracked-branches

    false

    Flag to indicate that the branch should be deleted locally if it’s origin tracked branch was removed.

    spring.cloud.config.server.git.force-pull

    false

    Flag to indicate that the repository should force pull. If true discard any local changes and take from remote repository.

    spring.cloud.config.server.git.host-key

     

    Valid SSH host key. Must be set if hostKeyAlgorithm is also set.

    spring.cloud.config.server.git.host-key-algorithm

     

    One of ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521. Must be set if hostKey is also set.

    spring.cloud.config.server.git.ignore-local-ssh-settings

    false

    If true, use property-based instead of file-based SSH config.

    spring.cloud.config.server.git.known-hosts-file

     

    Location of custom .known_hosts file.

    spring.cloud.config.server.git.order

     

    The order of the environment repository.

    spring.cloud.config.server.git.passphrase

     

    Passphrase for unlocking your ssh private key.

    spring.cloud.config.server.git.password

     

    Password for authentication with remote repository.

    spring.cloud.config.server.git.preferred-authentications

     

    Override server authentication method order. This should allow for evading login prompts if server has keyboard-interactive authentication before the publickey method.

    spring.cloud.config.server.git.private-key

     

    Valid SSH private key. Must be set if ignoreLocalSshSettings is true and Git URI is SSH format.

    spring.cloud.config.server.git.proxy

     

    HTTP proxy configuration.

    spring.cloud.config.server.git.refresh-rate

    0

    Time (in seconds) between refresh of the git repository.

    spring.cloud.config.server.git.repos

     

    Map of repository identifier to location and other properties.

    spring.cloud.config.server.git.search-paths

     

    Search paths to use within local working copy. By default searches only the root.

    spring.cloud.config.server.git.skip-ssl-validation

    false

    Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served over an HTTPS connection.

    spring.cloud.config.server.git.strict-host-key-checking

    true

    If false, ignore errors with host key.

    spring.cloud.config.server.git.timeout

    5

    Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable), defaults to 5 seconds.

    spring.cloud.config.server.git.uri

     

    URI of remote repository.

    spring.cloud.config.server.git.username

     

    Username for authentication with remote repository.

    spring.cloud.config.server.health.repositories

      

    spring.cloud.config.server.jdbc.order

    0

     

    spring.cloud.config.server.jdbc.sql

    SELECT KEY, VALUE from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=?

    SQL used to query database for keys and values.

    spring.cloud.config.server.native.add-label-locations

    true

    Flag to determine whether label locations should be added.

    spring.cloud.config.server.native.default-label

    master

     

    spring.cloud.config.server.native.fail-on-error

    false

    Flag to determine how to handle exceptions during decryption (default false).

    spring.cloud.config.server.native.order

      

    spring.cloud.config.server.native.search-locations

    []

    Locations to search for configuration files. Defaults to the same as a Spring Boot app so [classpath:/,classpath:/config/,file:./,file:./config/].

    spring.cloud.config.server.native.version

     

    Version string to be reported for native repository.

    spring.cloud.config.server.overrides

     

    Extra map for a property source to be sent to all clients unconditionally.

    spring.cloud.config.server.prefix

     

    Prefix for configuration resource paths (default is empty). Useful when embedding in another application when you don’t want to change the context path or servlet path.

    spring.cloud.config.server.strip-document-from-yaml

    true

    Flag to indicate that YAML documents that are text or collections (not a map) should be returned in "native" form.

    spring.cloud.config.server.svn.basedir

     

    Base directory for local working copy of repository.

    spring.cloud.config.server.svn.default-label

     

    The default label to be used with the remote repository.

    spring.cloud.config.server.svn.order

     

    The order of the environment repository.

    spring.cloud.config.server.svn.passphrase

     

    Passphrase for unlocking your ssh private key.

    spring.cloud.config.server.svn.password

     

    Password for authentication with remote repository.

    spring.cloud.config.server.svn.search-paths

     

    Search paths to use within local working copy. By default searches only the root.

    spring.cloud.config.server.svn.strict-host-key-checking

    true

    Reject incoming SSH host keys from remote servers not in the known host list.

    spring.cloud.config.server.svn.uri

     

    URI of remote repository.

    spring.cloud.config.server.svn.username

     

    Username for authentication with remote repository.

    spring.cloud.config.server.vault.backend

    secret

    Vault backend. Defaults to secret.

    spring.cloud.config.server.vault.default-key

    application

    The key in vault shared by all applications. Defaults to application. Set to empty to disable.

    spring.cloud.config.server.vault.host

    127.0.0.1

    Vault host. Defaults to 127.0.0.1.

    spring.cloud.config.server.vault.kv-version

    1

    Value to indicate which version of Vault kv backend is used. Defaults to 1.

    spring.cloud.config.server.vault.namespace

     

    The value of the Vault X-Vault-Namespace header. Defaults to null. This a Vault Enterprise feature only.

    spring.cloud.config.server.vault.order

      

    spring.cloud.config.server.vault.port

    8200

    Vault port. Defaults to 8200.

    spring.cloud.config.server.vault.profile-separator

    ,

    Vault profile separator. Defaults to comma.

    spring.cloud.config.server.vault.proxy

     

    HTTP proxy configuration.

    spring.cloud.config.server.vault.scheme

    http

    Vault scheme. Defaults to http.

    spring.cloud.config.server.vault.skip-ssl-validation

    false

    Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served over an HTTPS connection.

    spring.cloud.config.server.vault.timeout

    5

    Timeout (in seconds) for obtaining HTTP connection, defaults to 5 seconds.

    spring.cloud.config.token

     

    Security Token passed thru to underlying environment repository.

    spring.cloud.config.uri

    [http://localhost:8888]

    The URI of the remote server (default http://localhost:8888).

    spring.cloud.config.username

     

    The username to use (HTTP Basic) when contacting the remote server.

    spring.cloud.consul.config.acl-token

      

    spring.cloud.consul.config.data-key

    data

    If format is Format.PROPERTIES or Format.YAML then the following field is used as key to look up consul for configuration.

    spring.cloud.consul.config.default-context

    application

     

    spring.cloud.consul.config.enabled

    true

     

    spring.cloud.consul.config.fail-fast

    true

    Throw exceptions during config lookup if true, otherwise, log warnings.

    spring.cloud.consul.config.format

      

    spring.cloud.consul.config.name

     

    Alternative to spring.application.name to use in looking up values in consul KV.

    spring.cloud.consul.config.prefix

    config

     

    spring.cloud.consul.config.profile-separator

    ,

     

    spring.cloud.consul.config.watch.delay

    1000

    The value of the fixed delay for the watch in millis. Defaults to 1000.

    spring.cloud.consul.config.watch.enabled

    true

    If the watch is enabled. Defaults to true.

    spring.cloud.consul.config.watch.wait-time

    55

    The number of seconds to wait (or block) for watch query, defaults to 55. Needs to be less than default ConsulClient (defaults to 60). To increase ConsulClient timeout create a ConsulClient bean with a custom ConsulRawClient with a custom HttpClient.

    spring.cloud.consul.discovery.acl-token

      

    spring.cloud.consul.discovery.catalog-services-watch-delay

    1000

    The delay between calls to watch consul catalog in millis, default is 1000.

    spring.cloud.consul.discovery.catalog-services-watch-timeout

    2

    The number of seconds to block while watching consul catalog, default is 2.

    spring.cloud.consul.discovery.datacenters

     

    Map of serviceId’s → datacenter to query for in server list. This allows looking up services in another datacenters.

    spring.cloud.consul.discovery.default-query-tag

     

    Tag to query for in service list if one is not listed in serverListQueryTags.

    spring.cloud.consul.discovery.default-zone-metadata-name

    zone

    Service instance zone comes from metadata. This allows changing the metadata tag name.

    spring.cloud.consul.discovery.deregister

    true

    Disable automatic de-registration of service in consul.

    spring.cloud.consul.discovery.enabled

    true

    Is service discovery enabled?

    spring.cloud.consul.discovery.fail-fast

    true

    Throw exceptions during service registration if true, otherwise, log warnings (defaults to true).

    spring.cloud.consul.discovery.health-check-critical-timeout

     

    Timeout to deregister services critical for longer than timeout (e.g. 30m). Requires consul version 7.x or higher.

    spring.cloud.consul.discovery.health-check-headers

     

    Headers to be applied to the Health Check calls.

    spring.cloud.consul.discovery.health-check-interval

    10s

    How often to perform the health check (e.g. 10s), defaults to 10s.

    spring.cloud.consul.discovery.health-check-path

    /actuator/health

    Alternate server path to invoke for health checking.

    spring.cloud.consul.discovery.health-check-timeout

     

    Timeout for health check (e.g. 10s).

    spring.cloud.consul.discovery.health-check-tls-skip-verify

     

    Skips certificate verification during service checks if true, otherwise runs certificate verification.

    spring.cloud.consul.discovery.health-check-url

     

    Custom health check url to override default.

    spring.cloud.consul.discovery.heartbeat.enabled

    false

     

    spring.cloud.consul.discovery.heartbeat.interval-ratio

      

    spring.cloud.consul.discovery.heartbeat.ttl-unit

    s

     

    spring.cloud.consul.discovery.heartbeat.ttl-value

    30

     

    spring.cloud.consul.discovery.hostname

     

    Hostname to use when accessing server.

    spring.cloud.consul.discovery.instance-group

     

    Service instance group.

    spring.cloud.consul.discovery.instance-id

     

    Unique service instance id.

    spring.cloud.consul.discovery.instance-zone

     

    Service instance zone.

    spring.cloud.consul.discovery.ip-address

     

    IP address to use when accessing service (must also set preferIpAddress to use).

    spring.cloud.consul.discovery.lifecycle.enabled

    true

     

    spring.cloud.consul.discovery.management-port

     

    Port to register the management service under (defaults to management port).

    spring.cloud.consul.discovery.management-suffix

    management

    Suffix to use when registering management service.

    spring.cloud.consul.discovery.management-tags

     

    Tags to use when registering management service.

    spring.cloud.consul.discovery.order

    0

    Order of the discovery client used by CompositeDiscoveryClient for sorting available clients.

    spring.cloud.consul.discovery.port

     

    Port to register the service under (defaults to listening port).

    spring.cloud.consul.discovery.prefer-agent-address

    false

    Source of how we will determine the address to use.

    spring.cloud.consul.discovery.prefer-ip-address

    false

    Use ip address rather than hostname during registration.

    spring.cloud.consul.discovery.query-passing

    false

    Add the 'passing` parameter to /v1/health/service/serviceName. This pushes health check passing to the server.

    spring.cloud.consul.discovery.register

    true

    Register as a service in consul.

    spring.cloud.consul.discovery.register-health-check

    true

    Register health check in consul. Useful during development of a service.

    spring.cloud.consul.discovery.scheme

    http

    Whether to register an http or https service.

    spring.cloud.consul.discovery.server-list-query-tags

     

    Map of serviceId’s → tag to query for in server list. This allows filtering services by a single tag.

    spring.cloud.consul.discovery.service-name

     

    Service name.

    spring.cloud.consul.discovery.tags

     

    Tags to use when registering service.

    spring.cloud.consul.enabled

    true

    Is spring cloud consul enabled.

    spring.cloud.consul.host

    localhost

    Consul agent hostname. Defaults to 'localhost'.

    spring.cloud.consul.port

    8500

    Consul agent port. Defaults to '8500'.

    spring.cloud.consul.retry.initial-interval

    1000

    Initial retry interval in milliseconds.

    spring.cloud.consul.retry.max-attempts

    6

    Maximum number of attempts.

    spring.cloud.consul.retry.max-interval

    2000

    Maximum interval for backoff.

    spring.cloud.consul.retry.multiplier

    1.1

    Multiplier for next interval.

    spring.cloud.consul.scheme

     

    Consul agent scheme (HTTP/HTTPS). If there is no scheme in address - client will use HTTP.

    spring.cloud.consul.tls.certificate-password

     

    Password to open the certificate.

    spring.cloud.consul.tls.certificate-path

     

    File path to the certificate.

    spring.cloud.consul.tls.key-store-instance-type

     

    Type of key framework to use.

    spring.cloud.consul.tls.key-store-password

     

    Password to an external keystore.

    spring.cloud.consul.tls.key-store-path

     

    Path to an external keystore.

    spring.cloud.discovery.client.cloudfoundry.order

      

    spring.cloud.discovery.client.composite-indicator.enabled

    true

    Enables discovery client composite health indicator.

    spring.cloud.discovery.client.health-indicator.enabled

    true

     

    spring.cloud.discovery.client.health-indicator.include-description

    false

     

    spring.cloud.discovery.client.simple.instances

      

    spring.cloud.discovery.client.simple.local.instance-id

     

    The unique identifier or name for the service instance.

    spring.cloud.discovery.client.simple.local.metadata

     

    Metadata for the service instance. Can be used by discovery clients to modify their behaviour per instance, e.g. when load balancing.

    spring.cloud.discovery.client.simple.local.service-id

     

    The identifier or name for the service. Multiple instances might share the same service ID.

    spring.cloud.discovery.client.simple.local.uri

     

    The URI of the service instance. Will be parsed to extract the scheme, host, and port.

    spring.cloud.discovery.client.simple.order

      

    spring.cloud.discovery.enabled

    true

    Enables discovery client health indicators.

    spring.cloud.features.enabled

    true

    Enables the features endpoint.

    spring.cloud.function.compile

     

    Configuration for function bodies, which will be compiled. The key in the map is the function name and the value is a map containing a key "lambda" which is the body to compile, and optionally a "type" (defaults to "function"). Can also contain "inputType" and "outputType" in case it is ambiguous.

    spring.cloud.function.definition

     

    Name (e.g., 'foo') or composition instruction (e.g., 'foo|bar') used to resolve default function especially for cases where there is more then once function available in catalog.

    spring.cloud.function.imports

     

    Configuration for a set of files containing function bodies, which will be imported and compiled. The key in the map is the function name and the value is another map, containing a "location" of the file to compile and (optionally) a "type" (defaults to "function").

    spring.cloud.function.scan.packages

    functions

    Triggers scanning within the specified base packages for any class that is assignable to java.util.function.Function. For each detected Function class, a bean instance will be added to the context.

    spring.cloud.function.task.consumer

      

    spring.cloud.function.task.function

      

    spring.cloud.function.task.supplier

      

    spring.cloud.function.web.path

     

    Path to web resources for functions (should start with / if not empty).

    spring.cloud.function.web.supplier.auto-startup

    true

     

    spring.cloud.function.web.supplier.debug

    true

     

    spring.cloud.function.web.supplier.enabled

    false

     

    spring.cloud.function.web.supplier.headers

      

    spring.cloud.function.web.supplier.name

      

    spring.cloud.function.web.supplier.template-url

      

    spring.cloud.gateway.default-filters

     

    List of filter definitions that are applied to every route.

    spring.cloud.gateway.discovery.locator.enabled

    false

    Flag that enables DiscoveryClient gateway integration.

    spring.cloud.gateway.discovery.locator.filters

      

    spring.cloud.gateway.discovery.locator.include-expression

    true

    SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true.

    spring.cloud.gateway.discovery.locator.lower-case-service-id

    false

    Option to lower case serviceId in predicates and filters, defaults to false. Useful with eureka when it automatically uppercases serviceId. so MYSERIVCE, would match /myservice/**

    spring.cloud.gateway.discovery.locator.predicates

      

    spring.cloud.gateway.discovery.locator.route-id-prefix

     

    The prefix for the routeId, defaults to discoveryClient.getClass().getSimpleName() + "_". Service Id will be appended to create the routeId.

    spring.cloud.gateway.discovery.locator.url-expression

    'lb://'+serviceId

    SpEL expression that create the uri for each route, defaults to: 'lb://'+serviceId.

    spring.cloud.gateway.enabled

    true

    Enables gateway functionality.

    spring.cloud.gateway.filter.remove-hop-by-hop.headers

      

    spring.cloud.gateway.filter.remove-hop-by-hop.order

      

    spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key

    true

    Switch to deny requests if the Key Resolver returns an empty key, defaults to true.

    spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code

     

    HttpStatus to return when denyEmptyKey is true, defaults to FORBIDDEN.

    spring.cloud.gateway.filter.secure-headers.content-security-policy

    default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'

     

    spring.cloud.gateway.filter.secure-headers.content-type-options

    nosniff

     

    spring.cloud.gateway.filter.secure-headers.disable

      

    spring.cloud.gateway.filter.secure-headers.download-options

    noopen

     

    spring.cloud.gateway.filter.secure-headers.frame-options

    DENY

     

    spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies

    none

     

    spring.cloud.gateway.filter.secure-headers.referrer-policy

    no-referrer

     

    spring.cloud.gateway.filter.secure-headers.strict-transport-security

    max-age=631138519

     

    spring.cloud.gateway.filter.secure-headers.xss-protection-header

    1 ; mode=block

     

    spring.cloud.gateway.forwarded.enabled

    true

    Enables the ForwardedHeadersFilter.

    spring.cloud.gateway.globalcors.cors-configurations

      

    spring.cloud.gateway.httpclient.connect-timeout

     

    The connect timeout in millis, the default is 45s.

    spring.cloud.gateway.httpclient.max-header-size

     

    The max response header size.

    spring.cloud.gateway.httpclient.pool.acquire-timeout

     

    Only for type FIXED, the maximum time in millis to wait for aquiring.

    spring.cloud.gateway.httpclient.pool.max-connections

     

    Only for type FIXED, the maximum number of connections before starting pending acquisition on existing ones.

    spring.cloud.gateway.httpclient.pool.name

    proxy

    The channel pool map name, defaults to proxy.

    spring.cloud.gateway.httpclient.pool.type

     

    Type of pool for HttpClient to use, defaults to ELASTIC.

    spring.cloud.gateway.httpclient.proxy.host

     

    Hostname for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.proxy.non-proxy-hosts-pattern

     

    Regular expression (Java) for a configured list of hosts. that should be reached directly, bypassing the proxy

    spring.cloud.gateway.httpclient.proxy.password

     

    Password for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.proxy.port

     

    Port for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.proxy.username

     

    Username for proxy configuration of Netty HttpClient.

    spring.cloud.gateway.httpclient.response-timeout

     

    The response timeout.

    spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout

    3000ms

    SSL close_notify flush timeout. Default to 3000 ms.

    spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout-millis

      

    spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout

     

    SSL close_notify read timeout. Default to 0 ms.

    spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout-millis

      

    spring.cloud.gateway.httpclient.ssl.default-configuration-type

     

    The default ssl configuration type. Defaults to TCP.

    spring.cloud.gateway.httpclient.ssl.handshake-timeout

    10000ms

    SSL handshake timeout. Default to 10000 ms

    spring.cloud.gateway.httpclient.ssl.handshake-timeout-millis

      

    spring.cloud.gateway.httpclient.ssl.trusted-x509-certificates

     

    Trusted certificates for verifying the remote endpoint’s certificate.

    spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager

    false

    Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production.

    spring.cloud.gateway.httpclient.wiretap

    false

    Enables wiretap debugging for Netty HttpClient.

    spring.cloud.gateway.httpserver.wiretap

    false

    Enables wiretap debugging for Netty HttpServer.

    spring.cloud.gateway.loadbalancer.use404

    false

     

    spring.cloud.gateway.metrics.enabled

    true

    Enables the collection of metrics data.

    spring.cloud.gateway.proxy.headers

     

    Fixed header values that will be added to all downstream requests.

    spring.cloud.gateway.proxy.sensitive

     

    A set of sensitive header names that will not be sent downstream by default.

    spring.cloud.gateway.redis-rate-limiter.burst-capacity-header

    X-RateLimit-Burst-Capacity

    The name of the header that returns the burst capacity configuration.

    spring.cloud.gateway.redis-rate-limiter.config

      

    spring.cloud.gateway.redis-rate-limiter.include-headers

    true

    Whether or not to include headers containing rate limiter information, defaults to true.

    spring.cloud.gateway.redis-rate-limiter.remaining-header

    X-RateLimit-Remaining

    The name of the header that returns number of remaining requests during the current second.

    spring.cloud.gateway.redis-rate-limiter.replenish-rate-header

    X-RateLimit-Replenish-Rate

    The name of the header that returns the replenish rate configuration.

    spring.cloud.gateway.routes

     

    List of Routes.

    spring.cloud.gateway.streaming-media-types

      

    spring.cloud.gateway.x-forwarded.enabled

    true

    If the XForwardedHeadersFilter is enabled.

    spring.cloud.gateway.x-forwarded.for-append

    true

    If appending X-Forwarded-For as a list is enabled.

    spring.cloud.gateway.x-forwarded.for-enabled

    true

    If X-Forwarded-For is enabled.

    spring.cloud.gateway.x-forwarded.host-append

    true

    If appending X-Forwarded-Host as a list is enabled.

    spring.cloud.gateway.x-forwarded.host-enabled

    true

    If X-Forwarded-Host is enabled.

    spring.cloud.gateway.x-forwarded.order

    0

    The order of the XForwardedHeadersFilter.

    spring.cloud.gateway.x-forwarded.port-append

    true

    If appending X-Forwarded-Port as a list is enabled.

    spring.cloud.gateway.x-forwarded.port-enabled

    true

    If X-Forwarded-Port is enabled.

    spring.cloud.gateway.x-forwarded.prefix-append

    true

    If appending X-Forwarded-Prefix as a list is enabled.

    spring.cloud.gateway.x-forwarded.prefix-enabled

    true

    If X-Forwarded-Prefix is enabled.

    spring.cloud.gateway.x-forwarded.proto-append

    true

    If appending X-Forwarded-Proto as a list is enabled.

    spring.cloud.gateway.x-forwarded.proto-enabled

    true

    If X-Forwarded-Proto is enabled.

    spring.cloud.gcp.config.credentials.encoded-key

      

    spring.cloud.gcp.config.credentials.location

      

    spring.cloud.gcp.config.credentials.scopes

      

    spring.cloud.gcp.config.enabled

    false

    Enables Spring Cloud GCP Config.

    spring.cloud.gcp.config.name

     

    Name of the application.

    spring.cloud.gcp.config.profile

     

    Comma-delimited string of profiles under which the app is running. Gets its default value from the {@code spring.profiles.active} property, falling back on the {@code spring.profiles.default} property.

    spring.cloud.gcp.config.project-id

     

    Overrides the GCP project ID specified in the Core module.

    spring.cloud.gcp.config.timeout-millis

    60000

    Timeout for Google Runtime Configuration API calls.

    spring.cloud.gcp.credentials.encoded-key

      

    spring.cloud.gcp.credentials.location

      

    spring.cloud.gcp.credentials.scopes

      

    spring.cloud.gcp.datastore.credentials.encoded-key

      

    spring.cloud.gcp.datastore.credentials.location

      

    spring.cloud.gcp.datastore.credentials.scopes

      

    spring.cloud.gcp.datastore.namespace

      

    spring.cloud.gcp.datastore.project-id

      

    spring.cloud.gcp.logging.enabled

    true

    Auto-configure Google Cloud Stackdriver logging for Spring MVC.

    spring.cloud.gcp.project-id

     

    GCP project ID where services are running.

    spring.cloud.gcp.pubsub.credentials.encoded-key

      

    spring.cloud.gcp.pubsub.credentials.location

      

    spring.cloud.gcp.pubsub.credentials.scopes

      

    spring.cloud.gcp.pubsub.emulator-host

     

    The host and port of the local running emulator. If provided, this will setup the client to connect against a running pub/sub emulator.

    spring.cloud.gcp.pubsub.enabled

    true

    Auto-configure Google Cloud Pub/Sub components.

    spring.cloud.gcp.pubsub.project-id

     

    Overrides the GCP project ID specified in the Core module.

    spring.cloud.gcp.pubsub.publisher.batching.delay-threshold-seconds

     

    The delay threshold to use for batching. After this amount of time has elapsed (counting from the first element added), the elements will be wrapped up in a batch and sent.

    spring.cloud.gcp.pubsub.publisher.batching.element-count-threshold

     

    The element count threshold to use for batching.

    spring.cloud.gcp.pubsub.publisher.batching.enabled

     

    Enables batching if true.

    spring.cloud.gcp.pubsub.publisher.batching.flow-control.limit-exceeded-behavior

     

    The behavior when the specified limits are exceeded.

    spring.cloud.gcp.pubsub.publisher.batching.flow-control.max-outstanding-element-count

     

    Maximum number of outstanding elements to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.publisher.batching.flow-control.max-outstanding-request-bytes

     

    Maximum number of outstanding bytes to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.publisher.batching.request-byte-threshold

     

    The request byte threshold to use for batching.

    spring.cloud.gcp.pubsub.publisher.executor-threads

    4

    Number of threads used by every publisher.

    spring.cloud.gcp.pubsub.publisher.retry.initial-retry-delay-seconds

     

    InitialRetryDelay controls the delay before the first retry. Subsequent retries will use this value adjusted according to the RetryDelayMultiplier.

    spring.cloud.gcp.pubsub.publisher.retry.initial-rpc-timeout-seconds

     

    InitialRpcTimeout controls the timeout for the initial RPC. Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier.

    spring.cloud.gcp.pubsub.publisher.retry.jittered

     

    Jitter determines if the delay time should be randomized.

    spring.cloud.gcp.pubsub.publisher.retry.max-attempts

     

    MaxAttempts defines the maximum number of attempts to perform. If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout.

    spring.cloud.gcp.pubsub.publisher.retry.max-retry-delay-seconds

     

    MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier can’t increase the retry delay higher than this amount.

    spring.cloud.gcp.pubsub.publisher.retry.max-rpc-timeout-seconds

     

    MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier can’t increase the RPC timeout higher than this amount.

    spring.cloud.gcp.pubsub.publisher.retry.retry-delay-multiplier

     

    RetryDelayMultiplier controls the change in retry delay. The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call.

    spring.cloud.gcp.pubsub.publisher.retry.rpc-timeout-multiplier

     

    RpcTimeoutMultiplier controls the change in RPC timeout. The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call.

    spring.cloud.gcp.pubsub.publisher.retry.total-timeout-seconds

     

    TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. The higher the total timeout, the more retries can be attempted.

    spring.cloud.gcp.pubsub.subscriber.executor-threads

    4

    Number of threads used by every subscriber.

    spring.cloud.gcp.pubsub.subscriber.flow-control.limit-exceeded-behavior

     

    The behavior when the specified limits are exceeded.

    spring.cloud.gcp.pubsub.subscriber.flow-control.max-outstanding-element-count

     

    Maximum number of outstanding elements to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.subscriber.flow-control.max-outstanding-request-bytes

     

    Maximum number of outstanding bytes to keep in memory before enforcing flow control.

    spring.cloud.gcp.pubsub.subscriber.max-ack-extension-period

    0

    The optional max ack extension period in seconds for the subscriber factory.

    spring.cloud.gcp.pubsub.subscriber.max-acknowledgement-threads

    4

    Number of threads used for batch acknowledgement.

    spring.cloud.gcp.pubsub.subscriber.parallel-pull-count

     

    The optional parallel pull count setting for the subscriber factory.

    spring.cloud.gcp.pubsub.subscriber.pull-endpoint

     

    The optional pull endpoint setting for the subscriber factory.

    spring.cloud.gcp.pubsub.subscriber.retry.initial-retry-delay-seconds

     

    InitialRetryDelay controls the delay before the first retry. Subsequent retries will use this value adjusted according to the RetryDelayMultiplier.

    spring.cloud.gcp.pubsub.subscriber.retry.initial-rpc-timeout-seconds

     

    InitialRpcTimeout controls the timeout for the initial RPC. Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier.

    spring.cloud.gcp.pubsub.subscriber.retry.jittered

     

    Jitter determines if the delay time should be randomized.

    spring.cloud.gcp.pubsub.subscriber.retry.max-attempts

     

    MaxAttempts defines the maximum number of attempts to perform. If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout.

    spring.cloud.gcp.pubsub.subscriber.retry.max-retry-delay-seconds

     

    MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier can’t increase the retry delay higher than this amount.

    spring.cloud.gcp.pubsub.subscriber.retry.max-rpc-timeout-seconds

     

    MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier can’t increase the RPC timeout higher than this amount.

    spring.cloud.gcp.pubsub.subscriber.retry.retry-delay-multiplier

     

    RetryDelayMultiplier controls the change in retry delay. The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call.

    spring.cloud.gcp.pubsub.subscriber.retry.rpc-timeout-multiplier

     

    RpcTimeoutMultiplier controls the change in RPC timeout. The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call.

    spring.cloud.gcp.pubsub.subscriber.retry.total-timeout-seconds

     

    TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. The higher the total timeout, the more retries can be attempted.

    spring.cloud.gcp.security.iap.algorithm

    ES256

    Encryption algorithm used to sign the JWK token.

    spring.cloud.gcp.security.iap.audience

     

    Non-dynamic audience string to validate.

    spring.cloud.gcp.security.iap.enabled

    true

    Auto-configure Google Cloud IAP identity extraction components.

    spring.cloud.gcp.security.iap.header

    x-goog-iap-jwt-assertion

    Header from which to extract the JWK key.

    spring.cloud.gcp.security.iap.issuer

    https://cloud.google.com/iap

    JWK issuer to verify.

    spring.cloud.gcp.security.iap.registry

    https://www.gstatic.com/iap/verify/public_key-jwk

    Link to JWK public key registry.

    spring.cloud.gcp.spanner.create-interleaved-table-ddl-on-delete-cascade

    true

     

    spring.cloud.gcp.spanner.credentials.encoded-key

      

    spring.cloud.gcp.spanner.credentials.location

      

    spring.cloud.gcp.spanner.credentials.scopes

      

    spring.cloud.gcp.spanner.database

      

    spring.cloud.gcp.spanner.instance-id

      

    spring.cloud.gcp.spanner.keep-alive-interval-minutes

    -1

     

    spring.cloud.gcp.spanner.max-idle-sessions

    -1

     

    spring.cloud.gcp.spanner.max-sessions

    -1

     

    spring.cloud.gcp.spanner.min-sessions

    -1

     

    spring.cloud.gcp.spanner.num-rpc-channels

    -1

     

    spring.cloud.gcp.spanner.prefetch-chunks

    -1

     

    spring.cloud.gcp.spanner.project-id

      

    spring.cloud.gcp.spanner.write-sessions-fraction

    -1

     

    spring.cloud.gcp.sql.credentials

     

    Overrides the GCP OAuth2 credentials specified in the Core module.

    spring.cloud.gcp.sql.database-name

     

    Name of the database in the Cloud SQL instance.

    spring.cloud.gcp.sql.enabled

    true

    Auto-configure Google Cloud SQL support components.

    spring.cloud.gcp.sql.instance-connection-name

     

    Cloud SQL instance connection name. [GCP_PROJECT_ID]:[INSTANCE_REGION]:[INSTANCE_NAME].

    spring.cloud.gcp.storage.auto-create-files

      

    spring.cloud.gcp.storage.credentials.encoded-key

      

    spring.cloud.gcp.storage.credentials.location

      

    spring.cloud.gcp.storage.credentials.scopes

      

    spring.cloud.gcp.storage.enabled

    true

    Auto-configure Google Cloud Storage components.

    spring.cloud.gcp.trace.authority

     

    HTTP/2 authority the channel claims to be connecting to.

    spring.cloud.gcp.trace.compression

     

    Compression to use for the call.

    spring.cloud.gcp.trace.credentials.encoded-key

      

    spring.cloud.gcp.trace.credentials.location

      

    spring.cloud.gcp.trace.credentials.scopes

      

    spring.cloud.gcp.trace.deadline-ms

     

    Call deadline.

    spring.cloud.gcp.trace.enabled

    true

    Auto-configure Google Cloud Stackdriver tracing components.

    spring.cloud.gcp.trace.max-inbound-size

     

    Maximum size for an inbound message.

    spring.cloud.gcp.trace.max-outbound-size

     

    Maximum size for an outbound message.

    spring.cloud.gcp.trace.message-timeout

     

    Timeout in seconds before pending spans will be sent in batches to GCP Stackdriver Trace.

    spring.cloud.gcp.trace.num-executor-threads

    4

    Number of threads to be used by the Trace executor.

    spring.cloud.gcp.trace.project-id

     

    Overrides the GCP project ID specified in the Core module.

    spring.cloud.gcp.trace.wait-for-ready

     

    Waits for the channel to be ready in case of a transient failure. Defaults to failing fast in that case.

    spring.cloud.gcp.vision.credentials.encoded-key

      

    spring.cloud.gcp.vision.credentials.location

      

    spring.cloud.gcp.vision.credentials.scopes

      

    spring.cloud.gcp.vision.enabled

    true

    Auto-configure Google Cloud Vision components.

    spring.cloud.httpclientfactories.apache.enabled

    true

    Enables creation of Apache Http Client factory beans.

    spring.cloud.httpclientfactories.ok.enabled

    true

    Enables creation of OK Http Client factory beans.

    spring.cloud.hypermedia.refresh.fixed-delay

    5000

     

    spring.cloud.hypermedia.refresh.initial-delay

    10000

     

    spring.cloud.inetutils.default-hostname

    localhost

    The default hostname. Used in case of errors.

    spring.cloud.inetutils.default-ip-address

    127.0.0.1

    The default IP address. Used in case of errors.

    spring.cloud.inetutils.ignored-interfaces

     

    List of Java regular expressions for network interfaces that will be ignored.

    spring.cloud.inetutils.preferred-networks

     

    List of Java regular expressions for network addresses that will be preferred.

    spring.cloud.inetutils.timeout-seconds

    1

    Timeout, in seconds, for calculating hostname.

    spring.cloud.inetutils.use-only-site-local-interfaces

    false

    Whether to use only interfaces with site local addresses. See {@link InetAddress#isSiteLocalAddress()} for more details.

    spring.cloud.kubernetes.client.api-version

      

    spring.cloud.kubernetes.client.apiVersion

    v1

    Kubernetes API Version

    spring.cloud.kubernetes.client.ca-cert-data

      

    spring.cloud.kubernetes.client.ca-cert-file

      

    spring.cloud.kubernetes.client.caCertData

     

    Kubernetes API CACertData

    spring.cloud.kubernetes.client.caCertFile

     

    Kubernetes API CACertFile

    spring.cloud.kubernetes.client.client-cert-data

      

    spring.cloud.kubernetes.client.client-cert-file

      

    spring.cloud.kubernetes.client.client-key-algo

      

    spring.cloud.kubernetes.client.client-key-data

      

    spring.cloud.kubernetes.client.client-key-file

      

    spring.cloud.kubernetes.client.client-key-passphrase

      

    spring.cloud.kubernetes.client.clientCertData

     

    Kubernetes API ClientCertData

    spring.cloud.kubernetes.client.clientCertFile

     

    Kubernetes API ClientCertFile

    spring.cloud.kubernetes.client.clientKeyAlgo

    RSA

    Kubernetes API ClientKeyAlgo

    spring.cloud.kubernetes.client.clientKeyData

     

    Kubernetes API ClientKeyData

    spring.cloud.kubernetes.client.clientKeyFile

     

    Kubernetes API ClientKeyFile

    spring.cloud.kubernetes.client.clientKeyPassphrase

    changeit

    Kubernetes API ClientKeyPassphrase

    spring.cloud.kubernetes.client.connection-timeout

      

    spring.cloud.kubernetes.client.connectionTimeout

    10s

    Connection timeout

    spring.cloud.kubernetes.client.http-proxy

      

    spring.cloud.kubernetes.client.https-proxy

      

    spring.cloud.kubernetes.client.logging-interval

      

    spring.cloud.kubernetes.client.loggingInterval

    20s

    Logging interval

    spring.cloud.kubernetes.client.master-url

      

    spring.cloud.kubernetes.client.masterUrl

    https://kubernetes.default.svc

    Kubernetes API Master Node URL

    spring.cloud.kubernetes.client.namespace

    true

    Kubernetes Namespace

    spring.cloud.kubernetes.client.no-proxy

      

    spring.cloud.kubernetes.client.password

     

    Kubernetes API Password

    spring.cloud.kubernetes.client.proxy-password

      

    spring.cloud.kubernetes.client.proxy-username

      

    spring.cloud.kubernetes.client.request-timeout

      

    spring.cloud.kubernetes.client.requestTimeout

    10s

    Request timeout

    spring.cloud.kubernetes.client.rolling-timeout

      

    spring.cloud.kubernetes.client.rollingTimeout

    900s

    Rolling timeout

    spring.cloud.kubernetes.client.trust-certs

      

    spring.cloud.kubernetes.client.trustCerts

    false

    Kubernetes API Trust Certificates

    spring.cloud.kubernetes.client.username

     

    Kubernetes API Username

    spring.cloud.kubernetes.client.watch-reconnect-interval

      

    spring.cloud.kubernetes.client.watch-reconnect-limit

      

    spring.cloud.kubernetes.client.watchReconnectInterval

    1s

    Reconnect Interval

    spring.cloud.kubernetes.client.watchReconnectLimit

    -1

    Reconnect Interval limit retries

    spring.cloud.kubernetes.config.enable-api

    true

     

    spring.cloud.kubernetes.config.enabled

    true

    Enable the ConfigMap property source locator.

    spring.cloud.kubernetes.config.name

      

    spring.cloud.kubernetes.config.namespace

      

    spring.cloud.kubernetes.config.paths

      

    spring.cloud.kubernetes.config.sources

      

    spring.cloud.kubernetes.reload.enabled

    false

    Enables the Kubernetes configuration reload on change.

    spring.cloud.kubernetes.reload.mode

     

    Sets the detection mode for Kubernetes configuration reload.

    spring.cloud.kubernetes.reload.monitoring-config-maps

    true

    Enables monitoring on config maps to detect changes.

    spring.cloud.kubernetes.reload.monitoring-secrets

    false

    Enables monitoring on secrets to detect changes.

    spring.cloud.kubernetes.reload.period

    15000ms

    Sets the polling period to use when the detection mode is POLLING.

    spring.cloud.kubernetes.reload.strategy

     

    Sets the reload strategy for Kubernetes configuration reload on change.

    spring.cloud.kubernetes.secrets.enable-api

    false

     

    spring.cloud.kubernetes.secrets.enabled

    true

    Enable the Secrets property source locator.

    spring.cloud.kubernetes.secrets.labels

      

    spring.cloud.kubernetes.secrets.name

      

    spring.cloud.kubernetes.secrets.namespace

      

    spring.cloud.kubernetes.secrets.paths

      

    spring.cloud.loadbalancer.retry.enabled

    true

     

    spring.cloud.refresh.enabled

    true

    Enables autoconfiguration for the refresh scope and associated features.

    spring.cloud.refresh.extra-refreshable

    true

    Additional class names for beans to post process into refresh scope.

    spring.cloud.service-registry.auto-registration.enabled

    true

    Whether service auto-registration is enabled. Defaults to true.

    spring.cloud.service-registry.auto-registration.fail-fast

    false

    Whether startup fails if there is no AutoServiceRegistration. Defaults to false.

    spring.cloud.service-registry.auto-registration.register-management

    true

    Whether to register the management as a service. Defaults to true.

    spring.cloud.stream.binders

     

    Additional per-binder properties (see {@link BinderProperties}) if more then one binder of the same type is used (i.e., connect to multiple instances of RabbitMq). Here you can specify multiple binder configurations, each with different environment settings. For example; spring.cloud.stream.binders.rabbit1.environment. . . , spring.cloud.stream.binders.rabbit2.environment. . .

    spring.cloud.stream.binding-retry-interval

    30

    Retry interval (in seconds) used to schedule binding attempts. Default: 30 sec.

    spring.cloud.stream.bindings

     

    Additional binding properties (see {@link BinderProperties}) per binding name (e.g., 'input`). For example; This sets the content-type for the 'input' binding of a Sink application: 'spring.cloud.stream.bindings.input.contentType=text/plain'

    spring.cloud.stream.consul.binder.event-timeout

    5

     

    spring.cloud.stream.default-binder

     

    The name of the binder to use by all bindings in the event multiple binders available (e.g., 'rabbit').

    spring.cloud.stream.dynamic-destinations

    []

    A list of destinations that can be bound dynamically. If set, only listed destinations can be bound.

    spring.cloud.stream.function.definition

     

    Definition of functions to bind. If several functions need to be composed into one, use pipes (e.g., 'fooFunc\|barFunc')

    spring.cloud.stream.instance-count

    1

    The number of deployed instances of an application. Default: 1. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-count" where 'foo' is the name of the binding.

    spring.cloud.stream.instance-index

    0

    The instance id of the application: a number from 0 to instanceCount-1. Used for partitioning and with Kafka. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-index" where 'foo' is the name of the binding.

    spring.cloud.stream.integration.message-handler-not-propagated-headers

     

    Message header names that will NOT be copied from the inbound message.

    spring.cloud.stream.kafka.binder.auto-add-partitions

    false

     

    spring.cloud.stream.kafka.binder.auto-create-topics

    true

     

    spring.cloud.stream.kafka.binder.brokers

    [localhost]

     

    spring.cloud.stream.kafka.binder.configuration

     

    Arbitrary kafka properties that apply to both producers and consumers.

    spring.cloud.stream.kafka.binder.consumer-properties

     

    Arbitrary kafka consumer properties.

    spring.cloud.stream.kafka.binder.fetch-size

    0

     

    spring.cloud.stream.kafka.binder.header-mapper-bean-name

     

    The bean name of a custom header mapper to use instead of a {@link org.springframework.kafka.support.DefaultKafkaHeaderMapper}.

    spring.cloud.stream.kafka.binder.headers

    []

     

    spring.cloud.stream.kafka.binder.health-timeout

    60

    Time to wait to get partition information in seconds; default 60.

    spring.cloud.stream.kafka.binder.jaas

      

    spring.cloud.stream.kafka.binder.max-wait

    100

     

    spring.cloud.stream.kafka.binder.min-partition-count

    1

     

    spring.cloud.stream.kafka.binder.offset-update-count

    0

     

    spring.cloud.stream.kafka.binder.offset-update-shutdown-timeout

    2000

     

    spring.cloud.stream.kafka.binder.offset-update-time-window

    10000

     

    spring.cloud.stream.kafka.binder.producer-properties

     

    Arbitrary kafka producer properties.

    spring.cloud.stream.kafka.binder.queue-size

    8192

     

    spring.cloud.stream.kafka.binder.replication-factor

    1

     

    spring.cloud.stream.kafka.binder.required-acks

    1

     

    spring.cloud.stream.kafka.binder.socket-buffer-size

    2097152

     

    spring.cloud.stream.kafka.binder.transaction.producer.admin

      

    spring.cloud.stream.kafka.binder.transaction.producer.batch-timeout

      

    spring.cloud.stream.kafka.binder.transaction.producer.buffer-size

      

    spring.cloud.stream.kafka.binder.transaction.producer.compression-type

      

    spring.cloud.stream.kafka.binder.transaction.producer.configuration

      

    spring.cloud.stream.kafka.binder.transaction.producer.error-channel-enabled

      

    spring.cloud.stream.kafka.binder.transaction.producer.header-mode

      

    spring.cloud.stream.kafka.binder.transaction.producer.header-patterns

      

    spring.cloud.stream.kafka.binder.transaction.producer.message-key-expression

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-count

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-key-expression

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-key-extractor-name

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-selector-expression

      

    spring.cloud.stream.kafka.binder.transaction.producer.partition-selector-name

      

    spring.cloud.stream.kafka.binder.transaction.producer.required-groups

      

    spring.cloud.stream.kafka.binder.transaction.producer.sync

      

    spring.cloud.stream.kafka.binder.transaction.producer.topic

      

    spring.cloud.stream.kafka.binder.transaction.producer.use-native-encoding

      

    spring.cloud.stream.kafka.binder.transaction.transaction-id-prefix

      

    spring.cloud.stream.kafka.binder.zk-connection-timeout

    10000

    ZK Connection timeout in milliseconds.

    spring.cloud.stream.kafka.binder.zk-nodes

    [localhost]

     

    spring.cloud.stream.kafka.binder.zk-session-timeout

    10000

    ZK session timeout in milliseconds.

    spring.cloud.stream.kafka.bindings

      

    spring.cloud.stream.kafka.streams.binder.application-id

      

    spring.cloud.stream.kafka.streams.binder.auto-add-partitions

      

    spring.cloud.stream.kafka.streams.binder.auto-create-topics

      

    spring.cloud.stream.kafka.streams.binder.brokers

      

    spring.cloud.stream.kafka.streams.binder.configuration

      

    spring.cloud.stream.kafka.streams.binder.consumer-properties

      

    spring.cloud.stream.kafka.streams.binder.fetch-size

      

    spring.cloud.stream.kafka.streams.binder.header-mapper-bean-name

      

    spring.cloud.stream.kafka.streams.binder.headers

      

    spring.cloud.stream.kafka.streams.binder.health-timeout

      

    spring.cloud.stream.kafka.streams.binder.jaas

      

    spring.cloud.stream.kafka.streams.binder.max-wait

      

    spring.cloud.stream.kafka.streams.binder.min-partition-count

      

    spring.cloud.stream.kafka.streams.binder.offset-update-count

      

    spring.cloud.stream.kafka.streams.binder.offset-update-shutdown-timeout

      

    spring.cloud.stream.kafka.streams.binder.offset-update-time-window

      

    spring.cloud.stream.kafka.streams.binder.producer-properties

      

    spring.cloud.stream.kafka.streams.binder.queue-size

      

    spring.cloud.stream.kafka.streams.binder.replication-factor

      

    spring.cloud.stream.kafka.streams.binder.required-acks

      

    spring.cloud.stream.kafka.streams.binder.serde-error

     

    {@link org.apache.kafka.streams.errors.DeserializationExceptionHandler} to use when there is a Serde error. {@link KafkaStreamsBinderConfigurationProperties.SerdeError} values are used to provide the exception handler on consumer binding.

    spring.cloud.stream.kafka.streams.binder.socket-buffer-size

      

    spring.cloud.stream.kafka.streams.binder.zk-connection-timeout

      

    spring.cloud.stream.kafka.streams.binder.zk-nodes

      

    spring.cloud.stream.kafka.streams.binder.zk-session-timeout

      

    spring.cloud.stream.kafka.streams.bindings

      

    spring.cloud.stream.kafka.streams.time-window.advance-by

    0

     

    spring.cloud.stream.kafka.streams.time-window.length

    0

     

    spring.cloud.stream.metrics.export-properties

     

    List of properties that are going to be appended to each message. This gets populate by onApplicationEvent, once the context refreshes to avoid overhead of doing per message basis.

    spring.cloud.stream.metrics.key

     

    The name of the metric being emitted. Should be an unique value per application. Defaults to: ${spring.application.name:${vcap.application.name:${spring.config.name:application}}}.

    spring.cloud.stream.metrics.meter-filter

     

    Pattern to control the 'meters' one wants to capture. By default all 'meters' will be captured. For example, 'spring.integration.*' will only capture metric information for meters whose name starts with 'spring.integration'.

    spring.cloud.stream.metrics.properties

     

    Application properties that should be added to the metrics payload For example: spring.application**.

    spring.cloud.stream.metrics.schedule-interval

    60s

    Interval expressed as Duration for scheduling metrics snapshots publishing. Defaults to 60 seconds

    spring.cloud.stream.override-cloud-connectors

    false

    This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). The typical usage of this property is to be nested in a customized environment when connecting to multiple systems.

    spring.cloud.stream.rabbit.binder.admin-addresses

    []

    Urls for management plugins; only needed for queue affinity.

    spring.cloud.stream.rabbit.binder.admin-adresses

      

    spring.cloud.stream.rabbit.binder.compression-level

    0

    Compression level for compressed bindings; see 'java.util.zip.Deflator'.

    spring.cloud.stream.rabbit.binder.connection-name-prefix

     

    Prefix for connection names from this binder.

    spring.cloud.stream.rabbit.binder.nodes

    []

    Cluster member node names; only needed for queue affinity.

    spring.cloud.stream.rabbit.bindings

      

    spring.cloud.stream.schema-registry-client.cached

    false

     

    spring.cloud.stream.schema-registry-client.endpoint

      

    spring.cloud.stream.schema.avro.dynamic-schema-generation-enabled

    false

     

    spring.cloud.stream.schema.avro.prefix

    vnd

     

    spring.cloud.stream.schema.avro.reader-schema

      

    spring.cloud.stream.schema.avro.schema-imports

     

    A list of files or directories that should be loaded first thus making them importable by subsequent schemas. Note that imported files should not reference each other. @parameter

    spring.cloud.stream.schema.avro.schema-locations

     

    The source directory of Apache Avro schema. This schema is used by this converter. If this schema depends on other schemas consider defining those those dependent ones in the {@link #schemaImports} @parameter

    spring.cloud.stream.schema.server.allow-schema-deletion

    false

    Boolean flag to enable/disable schema deletion.

    spring.cloud.stream.schema.server.path

     

    Prefix for configuration resource paths (default is empty). Useful when embedding in another application when you don’t want to change the context path or servlet path.

    spring.cloud.task.batch.command-line-runner-order

    0

    The order for the {@code CommandLineRunner} used to run batch jobs when {@code spring.cloud.task.batch.fail-on-job-failure=true}. Defaults to 0 (same as the {@link org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner}).

    spring.cloud.task.batch.events.chunk-order

     

    Properties for chunk listener order

    spring.cloud.task.batch.events.chunk.enabled

    true

    This property is used to determine if a task should listen for batch chunk events.

    spring.cloud.task.batch.events.enabled

    true

    This property is used to determine if a task should listen for batch events.

    spring.cloud.task.batch.events.item-process-order

     

    Properties for itemProcess listener order

    spring.cloud.task.batch.events.item-process.enabled

    true

    This property is used to determine if a task should listen for batch item processed events.

    spring.cloud.task.batch.events.item-read-order

     

    Properties for itemRead listener order

    spring.cloud.task.batch.events.item-read.enabled

    true

    This property is used to determine if a task should listen for batch item read events.

    spring.cloud.task.batch.events.item-write-order

     

    Properties for itemWrite listener order

    spring.cloud.task.batch.events.item-write.enabled

    true

    This property is used to determine if a task should listen for batch item write events.

    spring.cloud.task.batch.events.job-execution-order

     

    Properties for jobExecution listener order

    spring.cloud.task.batch.events.job-execution.enabled

    true

    This property is used to determine if a task should listen for batch job execution events.

    spring.cloud.task.batch.events.skip-order

     

    Properties for skip listener order

    spring.cloud.task.batch.events.skip.enabled

    true

    This property is used to determine if a task should listen for batch skip events.

    spring.cloud.task.batch.events.step-execution-order

     

    Properties for stepExecution listener order

    spring.cloud.task.batch.events.step-execution.enabled

    true

    This property is used to determine if a task should listen for batch step execution events.

    spring.cloud.task.batch.fail-on-job-failure

    false

    This property is used to determine if a task app should return with a non zero exit code if a batch job fails.

    spring.cloud.task.batch.fail-on-job-failure-poll-interval

    5000

    Fixed delay in milliseconds that Spring Cloud Task will wait when checking if {@link org.springframework.batch.core.JobExecution}s have completed, when spring.cloud.task.batch.failOnJobFailure is set to true. Defaults to 5000.

    spring.cloud.task.batch.job-names

     

    Comma-separated list of job names to execute on startup (for instance, job1,job2). By default, all Jobs found in the context are executed.

    spring.cloud.task.batch.listener.enabled

    true

    This property is used to determine if a task will be linked to the batch jobs that are run.

    spring.cloud.task.closecontext-enabled

    false

    When set to true the context is closed at the end of the task. Else the context remains open.

    spring.cloud.task.events.enabled

    true

    This property is used to determine if a task app should emit task events.

    spring.cloud.task.executionid

     

    An id that will be used by the task when updating the task execution.

    spring.cloud.task.external-execution-id

     

    An id that can be associated with a task.

    spring.cloud.task.parent-execution-id

     

    The id of the parent task execution id that launched this task execution. Defaults to null if task execution had no parent.

    spring.cloud.task.single-instance-enabled

    false

    This property is used to determine if a task will execute if another task with the same app name is running.

    spring.cloud.task.single-instance-lock-check-interval

    500

    Declares the time (in millis) that a task execution will wait between checks. Default time is: 500 millis.

    spring.cloud.task.single-instance-lock-ttl

     

    Declares the maximum amount of time (in millis) that a task execution can hold a lock to prevent another task from executing with a specific task name when the single-instance-enabled is set to true. Default time is: Integer.MAX_VALUE.

    spring.cloud.task.table-prefix

    TASK_

    The prefix to append to the table names created by Spring Cloud Task.

    spring.cloud.util.enabled

    true

    Enables creation of Spring Cloud utility beans.

    spring.cloud.vault.app-id.app-id-path

    app-id

    Mount path of the AppId authentication backend.

    spring.cloud.vault.app-id.network-interface

     

    Network interface hint for the "MAC_ADDRESS" UserId mechanism.

    spring.cloud.vault.app-id.user-id

    MAC_ADDRESS

    UserId mechanism. Can be either "MAC_ADDRESS", "IP_ADDRESS", a string or a class name.

    spring.cloud.vault.app-role.app-role-path

    approle

    Mount path of the AppRole authentication backend.

    spring.cloud.vault.app-role.role

     

    Name of the role, optional, used for pull-mode.

    spring.cloud.vault.app-role.role-id

     

    The RoleId.

    spring.cloud.vault.app-role.secret-id

     

    The SecretId.

    spring.cloud.vault.application-name

    application

    Application name for AppId authentication.

    spring.cloud.vault.authentication

      

    spring.cloud.vault.aws-ec2.aws-ec2-path

    aws-ec2

    Mount path of the AWS-EC2 authentication backend.

    spring.cloud.vault.aws-ec2.identity-document

    http://169.254.169.254/latest/dynamic/instance-identity/pkcs7

    URL of the AWS-EC2 PKCS7 identity document.

    spring.cloud.vault.aws-ec2.nonce

     

    Nonce used for AWS-EC2 authentication. An empty nonce defaults to nonce generation.

    spring.cloud.vault.aws-ec2.role

     

    Name of the role, optional.

    spring.cloud.vault.aws-iam.aws-path

    aws

    Mount path of the AWS authentication backend.

    spring.cloud.vault.aws-iam.role

     

    Name of the role, optional. Defaults to the friendly IAM name if not set.

    spring.cloud.vault.aws-iam.server-name

     

    Name of the server used to set {@code X-Vault-AWS-IAM-Server-ID} header in the headers of login requests.

    spring.cloud.vault.aws.access-key-property

    cloud.aws.credentials.accessKey

    Target property for the obtained access key.

    spring.cloud.vault.aws.backend

    aws

    aws backend path.

    spring.cloud.vault.aws.enabled

    false

    Enable aws backend usage.

    spring.cloud.vault.aws.role

     

    Role name for credentials.

    spring.cloud.vault.aws.secret-key-property

    cloud.aws.credentials.secretKey

    Target property for the obtained secret key.

    spring.cloud.vault.azure-msi.azure-path

    azure

    Mount path of the Azure MSI authentication backend.

    spring.cloud.vault.azure-msi.role

     

    Name of the role.

    spring.cloud.vault.cassandra.backend

    cassandra

    Cassandra backend path.

    spring.cloud.vault.cassandra.enabled

    false

    Enable cassandra backend usage.

    spring.cloud.vault.cassandra.password-property

    spring.data.cassandra.password

    Target property for the obtained password.

    spring.cloud.vault.cassandra.role

     

    Role name for credentials.

    spring.cloud.vault.cassandra.username-property

    spring.data.cassandra.username

    Target property for the obtained username.

    spring.cloud.vault.config.lifecycle.enabled

    true

    Enable lifecycle management.

    spring.cloud.vault.config.order

    0

    Used to set a {@link org.springframework.core.env.PropertySource} priority. This is useful to use Vault as an override on other property sources. @see org.springframework.core.PriorityOrdered

    spring.cloud.vault.connection-timeout

    5000

    Connection timeout.

    spring.cloud.vault.consul.backend

    consul

    Consul backend path.

    spring.cloud.vault.consul.enabled

    false

    Enable consul backend usage.

    spring.cloud.vault.consul.role

     

    Role name for credentials.

    spring.cloud.vault.consul.token-property

    spring.cloud.consul.token

    Target property for the obtained token.

    spring.cloud.vault.database.backend

    database

    Database backend path.

    spring.cloud.vault.database.enabled

    false

    Enable database backend usage.

    spring.cloud.vault.database.password-property

    spring.datasource.password

    Target property for the obtained password.

    spring.cloud.vault.database.role

     

    Role name for credentials.

    spring.cloud.vault.database.username-property

    spring.datasource.username

    Target property for the obtained username.

    spring.cloud.vault.discovery.enabled

    false

    Flag to indicate that Vault server discovery is enabled (vault server URL will be looked up via discovery).

    spring.cloud.vault.discovery.service-id

    vault

    Service id to locate Vault.

    spring.cloud.vault.enabled

    true

    Enable Vault config server.

    spring.cloud.vault.fail-fast

    false

    Fail fast if data cannot be obtained from Vault.

    spring.cloud.vault.gcp-gce.gcp-path

    gcp

    Mount path of the Kubernetes authentication backend.

    spring.cloud.vault.gcp-gce.role

     

    Name of the role against which the login is being attempted.

    spring.cloud.vault.gcp-gce.service-account

     

    Optional service account id. Using the default id if left unconfigured.

    spring.cloud.vault.gcp-iam.credentials.encoded-key

     

    The base64 encoded contents of an OAuth2 account private key in JSON format.

    spring.cloud.vault.gcp-iam.credentials.location

     

    Location of the OAuth2 credentials private key. <p> Since this is a Resource, the private key can be in a multitude of locations, such as a local file system, classpath, URL, etc.

    spring.cloud.vault.gcp-iam.gcp-path

    gcp

    Mount path of the Kubernetes authentication backend.

    spring.cloud.vault.gcp-iam.jwt-validity

    15m

    Validity of the JWT token.

    spring.cloud.vault.gcp-iam.project-id

     

    Overrides the GCP project Id.

    spring.cloud.vault.gcp-iam.role

     

    Name of the role against which the login is being attempted.

    spring.cloud.vault.gcp-iam.service-account-id

     

    Overrides the GCP service account Id.

    spring.cloud.vault.generic.application-name

    application

    Application name to be used for the context.

    spring.cloud.vault.generic.backend

    secret

    Name of the default backend.

    spring.cloud.vault.generic.default-context

    application

    Name of the default context.

    spring.cloud.vault.generic.enabled

    true

    Enable the generic backend.

    spring.cloud.vault.generic.profile-separator

    /

    Profile-separator to combine application name and profile.

    spring.cloud.vault.host

    localhost

    Vault server host.

    spring.cloud.vault.kubernetes.kubernetes-path

    kubernetes

    Mount path of the Kubernetes authentication backend.

    spring.cloud.vault.kubernetes.role

     

    Name of the role against which the login is being attempted.

    spring.cloud.vault.kubernetes.service-account-token-file

    /var/run/secrets/kubernetes.io/serviceaccount/token

    Path to the service account token file.

    spring.cloud.vault.kv.application-name

    application

    Application name to be used for the context.

    spring.cloud.vault.kv.backend

    secret

    Name of the default backend.

    spring.cloud.vault.kv.backend-version

    2

    Key-Value backend version. Currently supported versions are: <ul> <li>Version 1 (unversioned key-value backend).</li> <li>Version 2 (versioned key-value backend).</li> </ul>

    spring.cloud.vault.kv.default-context

    application

    Name of the default context.

    spring.cloud.vault.kv.enabled

    false

    Enable the kev-value backend.

    spring.cloud.vault.kv.profile-separator

    /

    Profile-separator to combine application name and profile.

    spring.cloud.vault.mongodb.backend

    mongodb

    Cassandra backend path.

    spring.cloud.vault.mongodb.enabled

    false

    Enable mongodb backend usage.

    spring.cloud.vault.mongodb.password-property

    spring.data.mongodb.password

    Target property for the obtained password.

    spring.cloud.vault.mongodb.role

     

    Role name for credentials.

    spring.cloud.vault.mongodb.username-property

    spring.data.mongodb.username

    Target property for the obtained username.

    spring.cloud.vault.mysql.backend

    mysql

    mysql backend path.

    spring.cloud.vault.mysql.enabled

    false

    Enable mysql backend usage.

    spring.cloud.vault.mysql.password-property

    spring.datasource.password

    Target property for the obtained username.

    spring.cloud.vault.mysql.role

     

    Role name for credentials.

    spring.cloud.vault.mysql.username-property

    spring.datasource.username

    Target property for the obtained username.

    spring.cloud.vault.port

    8200

    Vault server port.

    spring.cloud.vault.postgresql.backend

    postgresql

    postgresql backend path.

    spring.cloud.vault.postgresql.enabled

    false

    Enable postgresql backend usage.

    spring.cloud.vault.postgresql.password-property

    spring.datasource.password

    Target property for the obtained username.

    spring.cloud.vault.postgresql.role

     

    Role name for credentials.

    spring.cloud.vault.postgresql.username-property

    spring.datasource.username

    Target property for the obtained username.

    spring.cloud.vault.rabbitmq.backend

    rabbitmq

    rabbitmq backend path.

    spring.cloud.vault.rabbitmq.enabled

    false

    Enable rabbitmq backend usage.

    spring.cloud.vault.rabbitmq.password-property

    spring.rabbitmq.password

    Target property for the obtained password.

    spring.cloud.vault.rabbitmq.role

     

    Role name for credentials.

    spring.cloud.vault.rabbitmq.username-property

    spring.rabbitmq.username

    Target property for the obtained username.

    spring.cloud.vault.read-timeout

    15000

    Read timeout.

    spring.cloud.vault.scheme

    https

    Protocol scheme. Can be either "http" or "https".

    spring.cloud.vault.ssl.cert-auth-path

    cert

    Mount path of the TLS cert authentication backend.

    spring.cloud.vault.ssl.key-store

     

    Trust store that holds certificates and private keys.

    spring.cloud.vault.ssl.key-store-password

     

    Password used to access the key store.

    spring.cloud.vault.ssl.trust-store

     

    Trust store that holds SSL certificates.

    spring.cloud.vault.ssl.trust-store-password

     

    Password used to access the trust store.

    spring.cloud.vault.token

     

    Static vault token. Required if {@link #authentication} is {@code TOKEN}.

    spring.cloud.vault.uri

     

    Vault URI. Can be set with scheme, host and port.

    spring.cloud.zookeeper.base-sleep-time-ms

    50

    Initial amount of time to wait between retries.

    spring.cloud.zookeeper.block-until-connected-unit

     

    The unit of time related to blocking on connection to Zookeeper.

    spring.cloud.zookeeper.block-until-connected-wait

    10

    Wait time to block on connection to Zookeeper.

    spring.cloud.zookeeper.connect-string

    localhost:2181

    Connection string to the Zookeeper cluster.

    spring.cloud.zookeeper.default-health-endpoint

     

    Default health endpoint that will be checked to verify that a dependency is alive.

    spring.cloud.zookeeper.dependencies

     

    Mapping of alias to ZookeeperDependency. From Ribbon perspective the alias is actually serviceID since Ribbon can’t accept nested structures in serviceID.

    spring.cloud.zookeeper.dependency-configurations

      

    spring.cloud.zookeeper.dependency-names

      

    spring.cloud.zookeeper.discovery.enabled

    true

     

    spring.cloud.zookeeper.discovery.initial-status

     

    The initial status of this instance (defaults to {@link StatusConstants#STATUS_UP}).

    spring.cloud.zookeeper.discovery.instance-host

     

    Predefined host with which a service can register itself in Zookeeper. Corresponds to the {code address} from the URI spec.

    spring.cloud.zookeeper.discovery.instance-id

     

    Id used to register with zookeeper. Defaults to a random UUID.

    spring.cloud.zookeeper.discovery.instance-port

     

    Port to register the service under (defaults to listening port).

    spring.cloud.zookeeper.discovery.instance-ssl-port

     

    Ssl port of the registered service.

    spring.cloud.zookeeper.discovery.metadata

     

    Gets the metadata name/value pairs associated with this instance. This information is sent to zookeeper and can be used by other instances.

    spring.cloud.zookeeper.discovery.order

    0

    Order of the discovery client used by CompositeDiscoveryClient for sorting available clients.

    spring.cloud.zookeeper.discovery.register

    true

    Register as a service in zookeeper.

    spring.cloud.zookeeper.discovery.root

    /services

    Root Zookeeper folder in which all instances are registered.

    spring.cloud.zookeeper.discovery.uri-spec

    {scheme}://{address}:{port}

    The URI specification to resolve during service registration in Zookeeper.

    spring.cloud.zookeeper.enabled

    true

    Is Zookeeper enabled.

    spring.cloud.zookeeper.max-retries

    10

    Max number of times to retry.

    spring.cloud.zookeeper.max-sleep-ms

    500

    Max time in ms to sleep on each retry.

    spring.cloud.zookeeper.prefix

     

    Common prefix that will be applied to all Zookeeper dependencies' paths.

    spring.integration.poller.fixed-delay

    1000

    Fixed delay for default poller.

    spring.integration.poller.max-messages-per-poll

    1

    Maximum messages per poll for the default poller.

    spring.sleuth.annotation.enabled

    true

     

    spring.sleuth.async.configurer.enabled

    true

    Enable default AsyncConfigurer.

    spring.sleuth.async.enabled

    true

    Enable instrumenting async related components so that the tracing information is passed between threads.

    spring.sleuth.async.ignored-beans

     

    List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation.

    spring.sleuth.baggage-keys

     

    List of baggage key names that should be propagated out of process. These keys will be prefixed with baggage before the actual key. This property is set in order to be backward compatible with previous Sleuth versions. @see brave.propagation.ExtraFieldPropagation.FactoryBuilder#addPrefixedFields(String, java.util.Collection)

    spring.sleuth.enabled

    true

     

    spring.sleuth.feign.enabled

    true

    Enable span information propagation when using Feign.

    spring.sleuth.feign.processor.enabled

    true

    Enable post processor that wraps Feign Context in its tracing representations.

    spring.sleuth.grpc.enabled

    true

    Enable span information propagation when using GRPC.

    spring.sleuth.http.enabled

    true

     

    spring.sleuth.http.legacy.enabled

    false

    Enables the legacy Sleuth setup.

    spring.sleuth.hystrix.strategy.enabled

    true

    Enable custom HystrixConcurrencyStrategy that wraps all Callable instances into their Sleuth representative - the TraceCallable.

    spring.sleuth.integration.enabled

    true

    Enable Spring Integration sleuth instrumentation.

    spring.sleuth.integration.patterns

    [!hystrixStreamOutput*, *]

    An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream channel name.

    spring.sleuth.integration.websockets.enabled

    true

    Enable tracing for WebSockets.

    spring.sleuth.keys.http.headers

     

    Additional headers that should be added as tags if they exist. If the header value is multi-valued, the tag value will be a comma-separated, single-quoted list.

    spring.sleuth.keys.http.prefix

    http.

    Prefix for header names if they are added as tags.

    spring.sleuth.log.slf4j.enabled

    true

    Enable a {@link Slf4jScopeDecorator} that prints tracing information in the logs.

    spring.sleuth.log.slf4j.whitelisted-mdc-keys

     

    A list of keys to be put from baggage to MDC.

    spring.sleuth.messaging.enabled

    false

    Should messaging be turned on.

    spring.sleuth.messaging.jms.enabled

    false

     

    spring.sleuth.messaging.jms.remote-service-name

    jms

     

    spring.sleuth.messaging.kafka.enabled

    false

     

    spring.sleuth.messaging.kafka.remote-service-name

    kafka

     

    spring.sleuth.messaging.rabbit.enabled

    false

     

    spring.sleuth.messaging.rabbit.remote-service-name

    rabbitmq

     

    spring.sleuth.opentracing.enabled

    true

     

    spring.sleuth.propagation-keys

     

    List of fields that are referenced the same in-process as it is on the wire. For example, the name "x-vcap-request-id" would be set as-is including the prefix. <p> Note: {@code fieldName} will be implicitly lower-cased. @see brave.propagation.ExtraFieldPropagation.FactoryBuilder#addField(String)

    spring.sleuth.propagation.tag.enabled

    true

    Enables a {@link TagPropagationFinishedSpanHandler} that adds extra propagated fields to span tags.

    spring.sleuth.propagation.tag.whitelisted-keys

     

    A list of keys to be put from extra propagation fields to span tags.

    spring.sleuth.reactor.decorate-on-each

    true

    When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries.

    spring.sleuth.reactor.enabled

    true

    When true enables instrumentation for reactor.

    spring.sleuth.rxjava.schedulers.hook.enabled

    true

    Enable support for RxJava via RxJavaSchedulersHook.

    spring.sleuth.rxjava.schedulers.ignoredthreads

    [HystrixMetricPoller, ^RxComputation.*$]

    Thread names for which spans will not be sampled.

    spring.sleuth.sampler.probability

    0.1

    Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there’s no support for 0.1% of the traces).

    spring.sleuth.sampler.rate

     

    A rate per second can be a nice choice for low-traffic endpoints as it allows you surge protection. For example, you may never expect the endpoint to get more than 50 requests per second. If there was a sudden surge of traffic, to 5000 requests per second, you would still end up with 50 traces per second. Conversely, if you had a percentage, like 10%, the same surge would end up with 500 traces per second, possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler (named Reservoir) for this purpose. Brave has taken the same approach via the {@link brave.sampler.RateLimitingSampler}.

    spring.sleuth.scheduled.enabled

    true

    Enable tracing for {@link org.springframework.scheduling.annotation.Scheduled}.

    spring.sleuth.scheduled.skip-pattern

    org.springframework.cloud.netflix.hystrix.stream.HystrixStreamTask

    Pattern for the fully qualified name of a class that should be skipped.

    spring.sleuth.supports-join

    true

    True means the tracing system supports sharing a span ID between a client and server.

    spring.sleuth.trace-id128

    false

    When true, generate 128-bit trace IDs instead of 64-bit ones.

    spring.sleuth.web.additional-skip-pattern

     

    Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}.

    spring.sleuth.web.client.enabled

    true

    Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}.

    spring.sleuth.web.client.skip-pattern

     

    Pattern for URLs that should be skipped in client side tracing.

    spring.sleuth.web.enabled

    true

    When true enables instrumentation for web applications.

    spring.sleuth.web.exception-logging-filter-enabled

    true

    Flag to toggle the presence of a filter that logs thrown exceptions.

    spring.sleuth.web.exception-throwing-filter-enabled

    true

    Flag to toggle the presence of a filter that logs thrown exceptions. @deprecated use {@link #exceptionLoggingFilterEnabled}

    spring.sleuth.web.filter-order

     

    Order in which the tracing filters should be registered. Defaults to {@link TraceHttpAutoConfiguration#TRACING_FILTER_ORDER}.

    spring.sleuth.web.ignore-auto-configured-skip-patterns

    false

    If set to true, auto-configured skip patterns will be ignored. @see TraceWebAutoConfiguration

    spring.sleuth.web.skip-pattern

    /api-docs.|/swagger.|.\.png|.\.css|.\.js|.\.html|/favicon.ico|/hystrix.stream

    Pattern for URLs that should be skipped in tracing.

    spring.sleuth.zuul.enabled

    true

    Enable span information propagation when using Zuul.

    spring.zipkin.base-url

    http://localhost:9411/

    URL of the zipkin query server instance. You can also provide the service id of the Zipkin server if Zipkin’s registered in service discovery (e.g. http://zipkinserver/).

    spring.zipkin.compression.enabled

    false

     

    spring.zipkin.discovery-client-enabled

     

    If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL always.

    spring.zipkin.enabled

    true

    Enables sending spans to Zipkin.

    spring.zipkin.encoder

     

    Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server is not recent.

    spring.zipkin.locator.discovery.enabled

    false

    Enabling of locating the host name via service discovery.

    spring.zipkin.message-timeout

    1

    Timeout in seconds before pending spans will be sent in batches to Zipkin.

    spring.zipkin.sender.type

     

    Means of sending spans to Zipkin.

    spring.zipkin.service.name

     

    The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin.

    stubrunner.amqp.enabled

    false

    Whether to enable support for Stub Runner and AMQP.

    stubrunner.amqp.mockCOnnection

    true

    Whether to enable support for Stub Runner and AMQP mocked connection factory.

    stubrunner.classifier

    stubs

    The classifier to use by default in ivy co-ordinates for a stub.

    stubrunner.cloud.consul.enabled

    true

    Whether to enable stubs registration in Consul.

    stubrunner.cloud.delegate.enabled

    true

    Whether to enable DiscoveryClient’s Stub Runner implementation.

    stubrunner.cloud.enabled

    true

    Whether to enable Spring Cloud support for Stub Runner.

    stubrunner.cloud.eureka.enabled

    true

    Whether to enable stubs registration in Eureka.

    stubrunner.cloud.ribbon.enabled

    true

    Whether to enable Stub Runner’s Ribbon integration.

    stubrunner.cloud.stubbed.discovery.enabled

    true

    Whether Service Discovery should be stubbed for Stub Runner. If set to false, stubs will get registered in real service discovery.

    stubrunner.cloud.zookeeper.enabled

    true

    Whether to enable stubs registration in Zookeeper.

    stubrunner.consumer-name

     

    You can override the default {@code spring.application.name} of this field by setting a value to this parameter.

    stubrunner.delete-stubs-after-test

    true

    If set to {@code false} will NOT delete stubs from a temporary folder after running tests.

    stubrunner.http-server-stub-configurer

     

    Configuration for an HTTP server stub.

    stubrunner.ids

    []

    The ids of the stubs to run in "ivy" notation ([groupId]:artifactId:[version]:[classifier][:port]). {@code groupId}, {@code classifier}, {@code version} and {@code port} can be optional.

    stubrunner.ids-to-service-ids

     

    Mapping of Ivy notation based ids to serviceIds inside your application. Example "a:b" → "myService" "artifactId" → "myOtherService"

    stubrunner.integration.enabled

    true

    Whether to enable Stub Runner integration with Spring Integration.

    stubrunner.mappings-output-folder

     

    Dumps the mappings of each HTTP server to the selected folder.

    stubrunner.max-port

    15000

    Max value of a port for the automatically started WireMock server.

    stubrunner.min-port

    10000

    Min value of a port for the automatically started WireMock server.

    stubrunner.password

     

    Repository password.

    stubrunner.properties

     

    Map of properties that can be passed to custom {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}.

    stubrunner.proxy-host

     

    Repository proxy host.

    stubrunner.proxy-port

     

    Repository proxy port.

    stubrunner.stream.enabled

    true

    Whether to enable Stub Runner integration with Spring Cloud Stream.

    stubrunner.stubs-mode

     

    Pick where the stubs should come from.

    stubrunner.stubs-per-consumer

    false

    Should only stubs for this particular consumer get registered in HTTP server stub.

    stubrunner.username

     

    Repository username.

    wiremock.rest-template-ssl-enabled

    false

     

    wiremock.server.files

    []

     

    wiremock.server.https-port

    -1

     

    wiremock.server.https-port-dynamic

    false

     

    wiremock.server.port

    8080

     

    wiremock.server.port-dynamic

    false

     

    wiremock.server.stubs

    []

     
    \ No newline at end of file diff --git a/Greenwich.SR5/spring-cloud.html b/Greenwich.SR5/spring-cloud.html new file mode 100644 index 00000000..c33aedfd --- /dev/null +++ b/Greenwich.SR5/spring-cloud.html @@ -0,0 +1,550 @@ + + + + + + + +Untitled + + + + + + + + + +
    +
    +
    +
    :linkcss:
    +:stylesdir: css
    +:stylesheet: manual-singlepage.css
    +
    +
    +
    +
    +
    = {docs-main}
    +
    +
    +
    +
    +
    {spring-cloud-version}
    +
    +
    +
    +
    +
    == Pick The Documentation Option
    +
    +
    +
    + +
    +
    + + + + + \ No newline at end of file diff --git a/Greenwich.SR5/spring-cloud.xml b/Greenwich.SR5/spring-cloud.xml new file mode 100644 index 00000000..e1b848df --- /dev/null +++ b/Greenwich.SR5/spring-cloud.xml @@ -0,0 +1,39686 @@ + + + + + +Spring Cloud +2020-02-03 + + + +Spring Cloud provides tools for developers to quickly build some of +the common patterns in distributed systems (e.g. configuration +management, service discovery, circuit breakers, intelligent routing, +micro-proxy, control bus). Coordination of +distributed systems leads to boiler plate patterns, and using Spring +Cloud developers can quickly stand up services and applications that +implement those patterns. They will work well in any distributed +environment, including the developer’s own laptop, bare metal data +centres, and managed platforms such as Cloud Foundry. +Version: Greenwich.SR5 + + +Features +Spring Cloud focuses on providing good out of box experience for typical use cases +and extensibility mechanism to cover others. + + +Distributed/versioned configuration + + +Service registration and discovery + + +Routing + + +Service-to-service calls + + +Load balancing + + +Circuit Breakers + + +Distributed messaging + + + + +Cloud Native Applications + +Cloud Native is a style of application development that encourages easy adoption of best practices in the areas of continuous delivery and value-driven development. +A related discipline is that of building 12-factor Applications, in which development practices are aligned with delivery and operations goals — for instance, by using declarative programming and management and monitoring. +Spring Cloud facilitates these styles of development in a number of specific ways. + The starting point is a set of features to which all components in a distributed system need easy access. +Many of those features are covered by Spring Boot, on which Spring Cloud builds. Some more features are delivered by Spring Cloud as two libraries: Spring Cloud Context and Spring Cloud Commons. +Spring Cloud Context provides utilities and special services for the ApplicationContext of a Spring Cloud application (bootstrap context, encryption, refresh scope, and environment endpoints). Spring Cloud Commons is a set of abstractions and common classes used in different Spring Cloud implementations (such as Spring Cloud Netflix and Spring Cloud Consul). +If you get an exception due to "Illegal key size" and you use Sun’s JDK, you need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. +See the following links for more information: + + +Java 6 JCE + + +Java 7 JCE + + +Java 8 JCE + + +Extract the files into the JDK/jre/lib/security folder for whichever version of JRE/JDK x64/x86 you use. + +Spring Cloud is released under the non-restrictive Apache 2.0 license. +If you would like to contribute to this section of the documentation or if you find an error, you can find the source code and issue trackers for the project at github. + + + +Spring Cloud Context: Application Context Services +Spring Boot has an opinionated view of how to build an application with Spring. +For instance, it has conventional locations for common configuration files and has endpoints for common management and monitoring tasks. +Spring Cloud builds on top of that and adds a few features that probably all components in a system would use or occasionally need. +
    +The Bootstrap Application Context +A Spring Cloud application operates by creating a bootstrap context, which is a parent context for the main application. +It is responsible for loading configuration properties from the external sources and for decrypting properties in the local external configuration files. +The two contexts share an Environment, which is the source of external properties for any Spring application. +By default, bootstrap properties (not bootstrap.properties but properties that are loaded during the bootstrap phase) are added with high precedence, so they cannot be overridden by local configuration. +The bootstrap context uses a different convention for locating external configuration than the main application context. +Instead of application.yml (or .properties), you can use bootstrap.yml, keeping the external configuration for bootstrap and main context +nicely separate. +The following listing shows an example: + +bootstrap.yml + +spring: + application: + name: foo + cloud: + config: + uri: ${SPRING_CONFIG_URI:http://localhost:8888} + + +If your application needs any application-specific configuration from the server, it is a good idea to set the spring.application.name (in bootstrap.yml or application.yml). +In order for the property spring.application.name to be used as the application’s context ID you +must set it in bootstrap.[properties | yml]. +If you want to retrieve specific profile configuration, you should also set spring.profiles.active in bootstrap.[properties | yml]. +You can disable the bootstrap process completely by setting spring.cloud.bootstrap.enabled=false (for example, in system properties). +
    +
    +Application Context Hierarchies +If you build an application context from SpringApplication or SpringApplicationBuilder, then the Bootstrap context is added as a parent to that context. +It is a feature of Spring that child contexts inherit property sources and profiles from their parent, so the main application context contains additional property sources, compared to building the same context without Spring Cloud Config. +The additional property sources are: + + +bootstrap: If any PropertySourceLocators are found in the Bootstrap context and if they have non-empty properties, an optional CompositePropertySource appears with high priority. +An example would be properties from the Spring Cloud Config Server. +See for instructions on how to customize the contents of this property source. + + +applicationConfig: [classpath:bootstrap.yml] (and related files if Spring profiles are active): If you have a bootstrap.yml (or .properties), those properties are used to configure the Bootstrap context. +Then they get added to the child context when its parent is set. +They have lower precedence than the application.yml (or .properties) and any other property sources that are added to the child as a normal part of the process of creating a Spring Boot application. +See for instructions on how to customize the contents of these property sources. + + +Because of the ordering rules of property sources, the bootstrap entries take precedence. +However, note that these do not contain any data from bootstrap.yml, which has very low precedence but can be used to set defaults. +You can extend the context hierarchy by setting the parent context of any ApplicationContext you create — for example, by using its own interface or with the SpringApplicationBuilder convenience methods (parent(), child() and sibling()). +The bootstrap context is the parent of the most senior ancestor that you create yourself. +Every context in the hierarchy has its own bootstrap (possibly empty) property source to avoid promoting values inadvertently from parents down to their descendants. +If there is a Config Server, every context in the hierarchy can also (in principle) have a different spring.application.name and, hence, a different remote property source. +Normal Spring application context behavior rules apply to property resolution: properties from a child context override those in +the parent, by name and also by property source name. +(If the child has a property source with the same name as the parent, the value from the parent is not included in the child). +Note that the SpringApplicationBuilder lets you share an Environment amongst the whole hierarchy, but that is not the default. +Thus, sibling contexts, in particular, do not need to have the same profiles or property sources, even though they may share common values with their parent. +
    +
    +Changing the Location of Bootstrap Properties +The bootstrap.yml (or .properties) location can be specified by setting spring.cloud.bootstrap.name (default: bootstrap), spring.cloud.bootstrap.location (default: empty) or spring.cloud.bootstrap.additional-location (default: empty) — for example, in System properties. +Those properties behave like the spring.config.* variants with the same name. +With spring.cloud.bootstrap.location the default locations are replaced and only the specified ones are used. +To add locations to the list of default ones, spring.cloud.bootstrap.additional-location could be used. +In fact, they are used to set up the bootstrap ApplicationContext by setting those properties in its Environment. +If there is an active profile (from spring.profiles.active or through the Environment API in the +context you are building), properties in that profile get loaded as well, the same as in a regular Spring Boot app — for example, from bootstrap-development.properties for a development profile. +
    +
    +Overriding the Values of Remote Properties +The property sources that are added to your application by the bootstrap context are often remote (from example, from Spring Cloud Config Server). +By default, they cannot be overridden locally. +If you want to let your applications override the remote properties with their own System properties or config files, the remote property source has to grant it permission by setting spring.cloud.config.allowOverride=true (it does not work to set this locally). +Once that flag is set, two finer-grained settings control the location of the remote properties in relation to system properties and the application’s local configuration: + + +spring.cloud.config.overrideNone=true: Override from any local property source. + + +spring.cloud.config.overrideSystemProperties=false: Only system properties, command line arguments, and environment variables (but not the local config files) should override the remote settings. + + +
    +
    +Customizing the Bootstrap Configuration +The bootstrap context can be set to do anything you like by adding entries to /META-INF/spring.factories under a key named org.springframework.cloud.bootstrap.BootstrapConfiguration. +This holds a comma-separated list of Spring @Configuration classes that are used to create the context. +Any beans that you want to be available to the main application context for autowiring can be created here. +There is a special contract for @Beans of type ApplicationContextInitializer. +If you want to control the startup sequence, classes can be marked with an @Order annotation (the default order is last). + +When adding custom BootstrapConfiguration, be careful that the classes you add are not @ComponentScanned by mistake into your main application context, where they might not be needed. +Use a separate package name for boot configuration classes and make sure that name is not already covered by your @ComponentScan or @SpringBootApplication annotated configuration classes. + +The bootstrap process ends by injecting initializers into the main SpringApplication instance (which is the normal Spring Boot startup sequence, whether it is running as a standalone application or deployed in an application server). +First, a bootstrap context is created from the classes found in spring.factories. +Then, all @Beans of type ApplicationContextInitializer are added to the main SpringApplication before it is started. +
    +
    +Customizing the Bootstrap Property Sources +The default property source for external configuration added by the bootstrap process is the Spring Cloud Config Server, but you can add additional sources by adding beans of type PropertySourceLocator to the bootstrap context (through spring.factories). +For instance, you can insert additional properties from a different server or from a database. +As an example, consider the following custom locator: +@Configuration +public class CustomPropertySourceLocator implements PropertySourceLocator { + + @Override + public PropertySource<?> locate(Environment environment) { + return new MapPropertySource("customProperty", + Collections.<String, Object>singletonMap("property.from.sample.custom.source", "worked as intended")); + } + +} +The Environment that is passed in is the one for the ApplicationContext about to be created — in other words, the one for which we supply additional property sources for. +It already has its normal Spring Boot-provided property sources, so you can use those to locate a property source specific to this Environment (for example, by keying it on spring.application.name, as is done in the default Spring Cloud Config Server property source locator). +If you create a jar with this class in it and then add a META-INF/spring.factories containing the following, the customProperty PropertySource appears in any application that includes that jar on its classpath: +org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomPropertySourceLocator +
    +
    +Logging Configuration +If you are going to use Spring Boot to configure log settings than +you should place this configuration in `bootstrap.[yml | properties] +if you would like it to apply to all events. + +For Spring Cloud to initialize logging configuration properly you cannot use a custom prefix. For example, +using custom.loggin.logpath will not be recognized by Spring Cloud when initializing the logging system. + +
    +
    +Environment Changes +The application listens for an EnvironmentChangeEvent and reacts to the change in a couple of standard ways (additional ApplicationListeners can be added as @Beans by the user in the normal way). +When an EnvironmentChangeEvent is observed, it has a list of key values that have changed, and the application uses those to: + + +Re-bind any @ConfigurationProperties beans in the context + + +Set the logger levels for any properties in logging.level.* + + +Note that the Config Client does not, by default, poll for changes in the Environment. +Generally, we would not recommend that approach for detecting changes (although you could set it up with a +@Scheduled annotation). +If you have a scaled-out client application, it is better to broadcast the EnvironmentChangeEvent to all the instances instead of having them polling for changes (for example, by using the Spring Cloud Bus). +The EnvironmentChangeEvent covers a large class of refresh use cases, as long as you can actually make a change to the Environment and publish the event. +Note that those APIs are public and part of core Spring). +You can verify that the changes are bound to @ConfigurationProperties beans by visiting the /configprops endpoint (a normal Spring Boot Actuator feature). +For instance, a DataSource can have its maxPoolSize changed at runtime (the default DataSource created by Spring Boot is an @ConfigurationProperties bean) and grow capacity dynamically. +Re-binding @ConfigurationProperties does not cover another large class of use cases, where you need more control over the refresh and where you need a change to be atomic over the whole ApplicationContext. +To address those concerns, we have @RefreshScope. +
    +
    +Refresh Scope +When there is a configuration change, a Spring @Bean that is marked as @RefreshScope gets special treatment. +This feature addresses the problem of stateful beans that only get their configuration injected when they are initialized. +For instance, if a DataSource has open connections when the database URL is changed via the Environment, you probably want the holders of those connections to be able to complete what they are doing. +Then, the next time something borrows a connection from the pool, it gets one with the new URL. +Sometimes, it might even be mandatory to apply the @RefreshScope +annotation on some beans which can be only initialized once. If a bean +is "immutable", you will have to either annotate the bean with @RefreshScope +or specify the classname under the property key +spring.cloud.refresh.extra-refreshable. + +If you create a DataSource bean yourself and the implementation is a HikariDataSource, return the +most specific type, in this case HikariDataSource. Otherwise, you will need to set +spring.cloud.refresh.extra-refreshable=javax.sql.DataSource. + +Refresh scope beans are lazy proxies that initialize when they are used (that is, when a method is called), and the scope acts as a cache of initialized values. +To force a bean to re-initialize on the next method call, you must invalidate its cache entry. +The RefreshScope is a bean in the context and has a public refreshAll() method to refresh all beans in the scope by clearing the target cache. +The /refresh endpoint exposes this functionality (over HTTP or JMX). +To refresh an individual bean by name, there is also a refresh(String) method. +To expose the /refresh endpoint, you need to add following configuration to your application: +management: + endpoints: + web: + exposure: + include: refresh + +@RefreshScope works (technically) on an @Configuration class, but it might lead to surprising behavior. +For example, it does not mean that all the @Beans defined in that class are themselves in @RefreshScope. +Specifically, anything that depends on those beans cannot rely on them being updated when a refresh is initiated, unless it is itself in @RefreshScope. +In that case, it is rebuilt on a refresh and its dependencies are re-injected. At that point, they are re-initialized from the refreshed @Configuration). + +
    +
    +Encryption and Decryption +Spring Cloud has an Environment pre-processor for decrypting property values locally. +It follows the same rules as the Config Server and has the same external configuration through encrypt.*. +Thus, you can use encrypted values in the form of {cipher}* and, as long as there is a valid key, they are decrypted before the main application context gets the Environment settings. +To use the encryption features in an application, you need to include Spring Security RSA in your classpath (Maven co-ordinates: "org.springframework.security:spring-security-rsa"), and you also need the full strength JCE extensions in your JVM. +If you get an exception due to "Illegal key size" and you use Sun’s JDK, you need to install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. +See the following links for more information: + + +Java 6 JCE + + +Java 7 JCE + + +Java 8 JCE + + +Extract the files into the JDK/jre/lib/security folder for whichever version of JRE/JDK x64/x86 you use. +
    +
    +Endpoints +For a Spring Boot Actuator application, some additional management endpoints are available. You can use: + + +POST to /actuator/env to update the Environment and rebind @ConfigurationProperties and log levels. + + +/actuator/refresh to re-load the boot strap context and refresh the @RefreshScope beans. + + +/actuator/restart to close the ApplicationContext and restart it (disabled by default). + + +/actuator/pause and /actuator/resume for calling the Lifecycle methods (stop() and start() on the ApplicationContext). + + + +If you disable the /actuator/restart endpoint then the /actuator/pause and /actuator/resume endpoints +will also be disabled since they are just a special case of /actuator/restart. + +
    +
    + +Spring Cloud Commons: Common Abstractions +Patterns such as service discovery, load balancing, and circuit breakers lend themselves to a common abstraction layer that can be consumed by all Spring Cloud clients, independent of the implementation (for example, discovery with Eureka or Consul). +
    +@EnableDiscoveryClient +Spring Cloud Commons provides the @EnableDiscoveryClient annotation. +This looks for implementations of the DiscoveryClient interface with META-INF/spring.factories. +Implementations of the Discovery Client add a configuration class to spring.factories under the org.springframework.cloud.client.discovery.EnableDiscoveryClient key. +Examples of DiscoveryClient implementations include Spring Cloud Netflix Eureka, Spring Cloud Consul Discovery, and Spring Cloud Zookeeper Discovery. +By default, implementations of DiscoveryClient auto-register the local Spring Boot server with the remote discovery server. +This behavior can be disabled by setting autoRegister=false in @EnableDiscoveryClient. + +@EnableDiscoveryClient is no longer required. +You can put a DiscoveryClient implementation on the classpath to cause the Spring Boot application to register with the service discovery server. + +
    +Health Indicator +Commons creates a Spring Boot HealthIndicator that DiscoveryClient implementations can participate in by implementing DiscoveryHealthIndicator. +To disable the composite HealthIndicator, set spring.cloud.discovery.client.composite-indicator.enabled=false. +A generic HealthIndicator based on DiscoveryClient is auto-configured (DiscoveryClientHealthIndicator). +To disable it, set spring.cloud.discovery.client.health-indicator.enabled=false. +To disable the description field of the DiscoveryClientHealthIndicator, set spring.cloud.discovery.client.health-indicator.include-description=false. +Otherwise, it can bubble up as the description of the rolled up HealthIndicator. +
    +
    +Ordering <literal>DiscoveryClient</literal> instances +DiscoveryClient interface extends Ordered. This is useful when using multiple discovery + clients, as it allows you to define the order of the returned discovery clients, similar to +how you can order the beans loaded by a Spring application. By default, the order of any DiscoveryClient is set to +0. If you want to set a different order for your custom DiscoveryClient implementations, you just need to override +the getOrder() method so that it returns the value that is suitable for your setup. Apart from this, you can use +properties to set the order of the DiscoveryClient +implementations provided by Spring Cloud, among others ConsulDiscoveryClient, EurekaDiscoveryClient and +ZookeeperDiscoveryClient. In order to do it, you just need to set the +spring.cloud.{clientIdentifier}.discovery.order (or eureka.client.order for Eureka) property to the desired value. +
    +
    +
    +ServiceRegistry +Commons now provides a ServiceRegistry interface that provides methods such as register(Registration) and deregister(Registration), which let you provide custom registered services. +Registration is a marker interface. +The following example shows the ServiceRegistry in use: +@Configuration +@EnableDiscoveryClient(autoRegister=false) +public class MyConfiguration { + private ServiceRegistry registry; + + public MyConfiguration(ServiceRegistry registry) { + this.registry = registry; + } + + // called through some external process, such as an event or a custom actuator endpoint + public void register() { + Registration registration = constructRegistration(); + this.registry.register(registration); + } +} +Each ServiceRegistry implementation has its own Registry implementation. + + +ZookeeperRegistration used with ZookeeperServiceRegistry + + +EurekaRegistration used with EurekaServiceRegistry + + +ConsulRegistration used with ConsulServiceRegistry + + +If you are using the ServiceRegistry interface, you are going to need to pass the +correct Registry implementation for the ServiceRegistry implementation you +are using. +
    +ServiceRegistry Auto-Registration +By default, the ServiceRegistry implementation auto-registers the running service. +To disable that behavior, you can set: +* @EnableDiscoveryClient(autoRegister=false) to permanently disable auto-registration. +* spring.cloud.service-registry.auto-registration.enabled=false to disable the behavior through configuration. +
    +ServiceRegistry Auto-Registration Events +There are two events that will be fired when a service auto-registers. The first event, called +InstancePreRegisteredEvent, is fired before the service is registered. The second +event, called InstanceRegisteredEvent, is fired after the service is registered. You can register an +ApplicationListener(s) to listen to and react to these events. + +These events will not be fired if spring.cloud.service-registry.auto-registration.enabled is set to false. + +
    +
    +
    +Service Registry Actuator Endpoint +Spring Cloud Commons provides a /service-registry actuator endpoint. +This endpoint relies on a Registration bean in the Spring Application Context. +Calling /service-registry with GET returns the status of the Registration. +Using POST to the same endpoint with a JSON body changes the status of the current Registration to the new value. +The JSON body has to include the status field with the preferred value. +Please see the documentation of the ServiceRegistry implementation you use for the allowed values when updating the status and the values returned for the status. +For instance, Eureka’s supported statuses are UP, DOWN, OUT_OF_SERVICE, and UNKNOWN. +
    +
    +
    +Spring RestTemplate as a Load Balancer Client +RestTemplate can be automatically configured to use a Load-balancer client under the hood. +To create a load-balanced RestTemplate, create a RestTemplate @Bean and use the @LoadBalanced qualifier, as shown in the following example: +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } +} + +public class MyClass { + @Autowired + private RestTemplate restTemplate; + + public String doOtherStuff() { + String results = restTemplate.getForObject("http://stores/stores", String.class); + return results; + } +} + +A RestTemplate bean is no longer created through auto-configuration. +Individual applications must create it. + +The URI needs to use a virtual host name (that is, a service name, not a host name). +The Ribbon client is used to create a full physical address. +See RibbonAutoConfiguration for details of how the RestTemplate is set up. + +In order to use a load-balanced RestTemplate, you need to have a load-balancer implementation in your classpath. +The recommended implementation is BlockingLoadBalancerClient +- add org.springframework.cloud:spring-cloud-loadbalancer in order to use it. +The +RibbonLoadBalancerClient also can be used, but it’s now under maintenance and we do not recommend adding it to new projects. + + +If you want to use BlockingLoadBalancerClient, make sure you do not have +RibbonLoadBalancerClient in the project classpath, as for backward compatibility reasons, it will be used by default. + +
    +
    +Spring WebClient as a Load Balancer Client +WebClient can be automatically configured to use a load-balancer client. +To create a load-balanced WebClient, create a WebClient.Builder @Bean and use the @LoadBalanced qualifier, as shown in the following example: +@Configuration +public class MyConfiguration { + + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } +} + +public class MyClass { + @Autowired + private WebClient.Builder webClientBuilder; + + public Mono<String> doOtherStuff() { + return webClientBuilder.build().get().uri("http://stores/stores") + .retrieve().bodyToMono(String.class); + } +} +The URI needs to use a virtual host name (that is, a service name, not a host name). +The Ribbon client is used to create a full physical address. + +If you want to use a @LoadBalanced WebClient.Builder, you need to have a loadbalancer +implementation in the classpath. It is recommended that you add the +org.springframework.cloud:spring-cloud-loadbalancer dependency to your project. +Then, ReactiveLoadBalancer will be used underneath. +Alternatively, this functionality will also work with spring-cloud-starter-netflix-ribbon, but the request +will be handled by a non-reactive LoadBalancerClient under the hood. Additionally, +spring-cloud-starter-netflix-ribbon is already in maintenance mode, so we do not recommned +adding it to new projects. + + +The ReactorLoadBalancer used underneath supports caching. If cacheManager is detected, +cached version of ServiceInstanceSupplier will be used. If not, we will retrieve instances +from discovery service without caching them. We recommend enabling caching in your project +if you use ReactiveLoadBalancer. + +
    +Retrying Failed Requests +A load-balanced RestTemplate can be configured to retry failed requests. +By default, this logic is disabled. +You can enable it by adding Spring Retry to your application’s classpath. +The load-balanced RestTemplate honors some of the Ribbon configuration values related to retrying failed requests. +You can use client.ribbon.MaxAutoRetries, client.ribbon.MaxAutoRetriesNextServer, and client.ribbon.OkToRetryOnAllOperations properties. +If you would like to disable the retry logic with Spring Retry on the classpath, you can set spring.cloud.loadbalancer.retry.enabled=false. +See the Ribbon documentation for a description of what these properties do. +If you would like to implement a BackOffPolicy in your retries, you need to create a bean of type LoadBalancedRetryFactory and override the createBackOffPolicy method: +@Configuration +public class MyConfiguration { + @Bean + LoadBalancedRetryFactory retryFactory() { + return new LoadBalancedRetryFactory() { + @Override + public BackOffPolicy createBackOffPolicy(String service) { + return new ExponentialBackOffPolicy(); + } + }; + } +} + +client in the preceding examples should be replaced with your Ribbon client’s name. + +If you want to add one or more RetryListener implementations to your retry functionality, you need to +create a bean of type LoadBalancedRetryListenerFactory and return the RetryListener array +you would like to use for a given service, as shown in the following example: +@Configuration +public class MyConfiguration { + @Bean + LoadBalancedRetryListenerFactory retryListenerFactory() { + return new LoadBalancedRetryListenerFactory() { + @Override + public RetryListener[] createRetryListeners(String service) { + return new RetryListener[]{new RetryListener() { + @Override + public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) { + //TODO Do you business... + return true; + } + + @Override + public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { + //TODO Do you business... + } + + @Override + public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { + //TODO Do you business... + } + }}; + } + }; + } +} +
    +
    +
    +Multiple RestTemplate objects +If you want a RestTemplate that is not load-balanced, create a RestTemplate bean and inject it. +To access the load-balanced RestTemplate, use the @LoadBalanced qualifier when you create your @Bean, as shown in the following example: +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + RestTemplate loadBalanced() { + return new RestTemplate(); + } + + @Primary + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } +} + +public class MyClass { +@Autowired +private RestTemplate restTemplate; + + @Autowired + @LoadBalanced + private RestTemplate loadBalanced; + + public String doOtherStuff() { + return loadBalanced.getForObject("http://stores/stores", String.class); + } + + public String doStuff() { + return restTemplate.getForObject("https://example.com", String.class); + } +} + +Notice the use of the @Primary annotation on the plain RestTemplate declaration in the preceding example to disambiguate the unqualified @Autowired injection. + + +If you see errors such as java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89, try injecting RestOperations or setting spring.aop.proxyTargetClass=true. + +
    +
    +Multiple WebClient Objects +If you want a WebClient that is not load-balanced, create a WebClient bean and inject it. +To access the load-balanced WebClient, use the @LoadBalanced qualifier when you create your @Bean, as shown in the following example: +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + WebClient.Builder loadBalanced() { + return WebClient.builder(); + } + + @Primary + @Bean + WebClient.Builder webClient() { + return WebClient.builder(); + } +} + +public class MyClass { + @Autowired + private WebClient.Builder webClientBuilder; + + @Autowired + @LoadBalanced + private WebClient.Builder loadBalanced; + + public Mono<String> doOtherStuff() { + return loadBalanced.build().get().uri("http://stores/stores") + .retrieve().bodyToMono(String.class); + } + + public Mono<String> doStuff() { + return webClientBuilder.build().get().uri("http://example.com") + .retrieve().bodyToMono(String.class); + } +} +
    +
    +Spring WebFlux WebClient as a Load Balancer Client +
    +Spring WebFlux WebClient with Reactive Load Balancer +WebClient can be configured to use the ReactiveLoadBalancer. +If you add org.springframework.cloud:spring-cloud-loadbalancer to your project, + ReactorLoadBalancerExchangeFilterFunction is auto-configured if spring-webflux is on the classpath. +The following example shows how to configure a WebClient to use reactive load balancer under the hood: +public class MyClass { + @Autowired + private ReactorLoadBalancerExchangeFilterFunction lbFunction; + + public Mono<String> doOtherStuff() { + return WebClient.builder().baseUrl("http://stores") + .filter(lbFunction) + .build() + .get() + .uri("/stores") + .retrieve() + .bodyToMono(String.class); + } +} +The URI needs to use a virtual host name (that is, a service name, not a host name). +The ReactorLoadBalancerClient is used to create a full physical address. +
    +
    +Spring WebFlux WebClient with non-reactive Load Balancer Client +If you you don’t have org.springframework.cloud:spring-cloud-loadbalancer in your project, +but you do have spring-cloud-starter-netflix-ribbon, you can still use WebClient with LoadBalancerClient. LoadBalancerExchangeFilterFunction +will be auto-configured if spring-webflux is on the classpath. Please note, however, that this is +uses a non-reactive client under the hood. +The following example shows how to configure a WebClient to use load balancer: +public class MyClass { + @Autowired + private LoadBalancerExchangeFilterFunction lbFunction; + + public Mono<String> doOtherStuff() { + return WebClient.builder().baseUrl("http://stores") + .filter(lbFunction) + .build() + .get() + .uri("/stores") + .retrieve() + .bodyToMono(String.class); + } +} +The URI needs to use a virtual host name (that is, a service name, not a host name). +The LoadBalancerClient is used to create a full physical address. +WARN: This approach is now deprecated. +We suggest you use WebFlux with reactive Load-Balancer +instead. +
    +
    +Passing your own Load-Balancer Client configuration +You can also use the @LoadBalancerClient annotation to pass your own load-balancer client configuration, passing the name of the load-balancer client and the configuration class, like so: +@Configuration +@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class) +public class MyConfiguration { + + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } +} +It is also possible to pass together multiple configurations (for more than one load-balancer client) via the @LoadBalancerClients annotation, as shown below: +@Configuration +@LoadBalancerClients({@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class), @LoadBalancerClient(value = "customers", configuration = CustomersLoadBalancerClientConfiguration.class)}) +public class MyConfiguration { + + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } +} +
    +
    +
    +Ignore Network Interfaces +Sometimes, it is useful to ignore certain named network interfaces so that they can be excluded from Service Discovery registration (for example, when running in a Docker container). +A list of regular expressions can be set to cause the desired network interfaces to be ignored. +The following configuration ignores the docker0 interface and all interfaces that start with veth: + +application.yml + +spring: + cloud: + inetutils: + ignoredInterfaces: + - docker0 + - veth.* + + +You can also force the use of only specified network addresses by using a list of regular expressions, as shown in the following example: + +bootstrap.yml + +spring: + cloud: + inetutils: + preferredNetworks: + - 192.168 + - 10.0 + + +You can also force the use of only site-local addresses, as shown in the following example: +.application.yml +spring: + cloud: + inetutils: + useOnlySiteLocalInterfaces: true +See Inet4Address.html.isSiteLocalAddress() for more details about what constitutes a site-local address. +
    +
    +HTTP Client Factories +Spring Cloud Commons provides beans for creating both Apache HTTP clients (ApacheHttpClientFactory) and OK HTTP clients (OkHttpClientFactory). +The OkHttpClientFactory bean is created only if the OK HTTP jar is on the classpath. +In addition, Spring Cloud Commons provides beans for creating the connection managers used by both clients: ApacheHttpClientConnectionManagerFactory for the Apache HTTP client and OkHttpClientConnectionPoolFactory for the OK HTTP client. +If you would like to customize how the HTTP clients are created in downstream projects, you can provide your own implementation of these beans. +In addition, if you provide a bean of type HttpClientBuilder or OkHttpClient.Builder, the default factories use these builders as the basis for the builders returned to downstream projects. +You can also disable the creation of these beans by setting spring.cloud.httpclientfactories.apache.enabled or spring.cloud.httpclientfactories.ok.enabled to false. +
    +
    +Enabled Features +Spring Cloud Commons provides a /features actuator endpoint. +This endpoint returns features available on the classpath and whether they are enabled. +The information returned includes the feature type, name, version, and vendor. +
    +Feature types +There are two types of 'features': abstract and named. +Abstract features are features where an interface or abstract class is defined and that an implementation the creates, such as DiscoveryClient, LoadBalancerClient, or LockService. +The abstract class or interface is used to find a bean of that type in the context. +The version displayed is bean.getClass().getPackage().getImplementationVersion(). +Named features are features that do not have a particular class they implement, such as "Circuit Breaker", "API Gateway", "Spring Cloud Bus", and others. These features require a name and a bean type. +
    +
    +Declaring features +Any module can declare any number of HasFeature beans, as shown in the following examples: +@Bean +public HasFeatures commonsFeatures() { + return HasFeatures.abstractFeatures(DiscoveryClient.class, LoadBalancerClient.class); +} + +@Bean +public HasFeatures consulFeatures() { + return HasFeatures.namedFeatures( + new NamedFeature("Spring Cloud Bus", ConsulBusAutoConfiguration.class), + new NamedFeature("Circuit Breaker", HystrixCommandAspect.class)); +} + +@Bean +HasFeatures localFeatures() { + return HasFeatures.builder() + .abstractFeature(Foo.class) + .namedFeature(new NamedFeature("Bar Feature", Bar.class)) + .abstractFeature(Baz.class) + .build(); +} +Each of these beans should go in an appropriately guarded @Configuration. +
    +
    +
    +Spring Cloud Compatibility Verification +Due to the fact that some users have problem with setting up Spring Cloud application, we’ve decided +to add a compatibility verification mechanism. It will break if your current setup is not compatible +with Spring Cloud requirements, together with a report, showing what exactly went wrong. +At the moment we verify which version of Spring Boot is added to your classpath. +Example of a report +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +Your project setup is incompatible with our requirements due to following reasons: + +- Spring Boot [2.1.0.RELEASE] is not compatible with this Spring Cloud release train + + +Action: + +Consider applying the following actions: + +- Change Spring Boot version to one of the following versions [1.2.x, 1.3.x] . +You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn]. +If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section. +In order to disable this feature, set spring.cloud.compatibility-verifier.enabled to false. +If you want to override the compatible Spring Boot versions, just set the +spring.cloud.compatibility-verifier.compatible-boot-versions property with a comma separated list +of compatible Spring Boot versions. +
    +
    +
    + +Spring Cloud Config + +Greenwich.SR5 +Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments. +The concepts on both client and server map identically to the Spring Environment and PropertySource abstractions, so they fit very well with Spring applications but can be used with any application running in any language. +As an application moves through the deployment pipeline from dev to test and into production, you can manage the configuration between those environments and be certain that applications have everything they need to run when they migrate. +The default implementation of the server storage backend uses git, so it easily supports labelled versions of configuration environments as well as being accessible to a wide range of tooling for managing the content. +It is easy to add alternative implementations and plug them in with Spring configuration. + + +Quick Start +This quick start walks through using both the server and the client of Spring Cloud Config Server. +First, start the server, as follows: +$ cd spring-cloud-config-server +$ ../mvnw spring-boot:run +The server is a Spring Boot application, so you can run it from your IDE if you prefer to do so (the main class is ConfigServerApplication). +Next try out a client, as follows: +$ curl localhost:8888/foo/development +{"name":"foo","label":"master","propertySources":[ + {"name":"https://github.com/scratches/config-repo/foo-development.properties","source":{"bar":"spam"}}, + {"name":"https://github.com/scratches/config-repo/foo.properties","source":{"foo":"bar"}} +]} +The default strategy for locating property sources is to clone a git repository (at spring.cloud.config.server.git.uri) and use it to initialize a mini SpringApplication. +The mini-application’s Environment is used to enumerate property sources and publish them at a JSON endpoint. +The HTTP service has resources in the following form: +/{application}/{profile}[/{label}] +/{application}-{profile}.yml +/{label}/{application}-{profile}.yml +/{application}-{profile}.properties +/{label}/{application}-{profile}.properties +where application is injected as the spring.config.name in the SpringApplication (what is normally application in a regular Spring Boot app), profile is an active profile (or comma-separated list of properties), and label is an optional git label (defaults to master.) +Spring Cloud Config Server pulls configuration for remote clients from various sources. The following example gets configuration from a git repository (which must be provided), as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo +Other sources are any JDBC compatible database, Subversion, Hashicorp Vault, Credhub and local filesystems. +
    +Client Side Usage +To use these features in an application, you can build it as a Spring Boot application that depends on spring-cloud-config-client (for an example, see the test cases for the config-client or the sample application). +The most convenient way to add the dependency is with a Spring Boot starter org.springframework.cloud:spring-cloud-starter-config. +There is also a parent pom and BOM (spring-cloud-starter-parent) for Maven users and a Spring IO version management properties file for Gradle and Spring CLI users. The following example shows a typical Maven configuration: + +pom.xml + + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>{spring-boot-docs-version}</version> + <relativePath /> <!-- lookup parent from repository --> + </parent> + +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>{spring-cloud-version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> + +<dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-config</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> +</dependencies> + +<build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + </plugins> +</build> + + <!-- repositories also needed for snapshots and milestones --> + + +Now you can create a standard Spring Boot application, such as the following HTTP server: +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +When this HTTP server runs, it picks up the external configuration from the default local config server (if it is running) on port 8888. +To modify the startup behavior, you can change the location of the config server by using bootstrap.properties (similar to application.properties but for the bootstrap phase of an application context), as shown in the following example: +spring.cloud.config.uri: http://myconfigserver.com +By default, if no application name is set, application will be used. To modify the name, the following property can be added to the bootstrap.properties file: +spring.application.name: myapp + +When setting the property ${spring.application.name} do not prefix your app name with the reserved word application- to prevent issues resolving the correct property source. + +The bootstrap properties show up in the /env endpoint as a high-priority property source, as shown in the following example. +$ curl localhost:8080/env +{ + "profiles":[], + "configService:https://github.com/spring-cloud-samples/config-repo/bar.properties":{"foo":"bar"}, + "servletContextInitParams":{}, + "systemProperties":{...}, + ... +} +A property source called ``configService:<URL of remote repository>/<file name> contains the foo property with a value of bar and is highest priority. + +The URL in the property source name is the git repository, not the config server URL. + +
    +
    + +Spring Cloud Config Server +Spring Cloud Config Server provides an HTTP resource-based API for external configuration (name-value pairs or equivalent YAML content). +The server is embeddable in a Spring Boot application, by using the @EnableConfigServer annotation. +Consequently, the following application is a config server: + +ConfigServer.java + +@SpringBootApplication +@EnableConfigServer +public class ConfigServer { + public static void main(String[] args) { + SpringApplication.run(ConfigServer.class, args); + } +} + + +Like all Spring Boot applications, it runs on port 8080 by default, but you can switch it to the more conventional port 8888 in various ways. +The easiest, which also sets a default configuration repository, is by launching it with spring.config.name=configserver (there is a configserver.yml in the Config Server jar). +Another is to use your own application.properties, as shown in the following example: + +application.properties + +server.port: 8888 +spring.cloud.config.server.git.uri: file://${user.home}/config-repo + + +where ${user.home}/config-repo is a git repository containing YAML and properties files. + +On Windows, you need an extra "/" in the file URL if it is absolute with a drive prefix (for example,file:///${user.home}/config-repo). + + +The following listing shows a recipe for creating the git repository in the preceding example: +$ cd $HOME +$ mkdir config-repo +$ cd config-repo +$ git init . +$ echo info.foo: bar > application.properties +$ git add -A . +$ git commit -m "Add application.properties" + + +Using the local filesystem for your git repository is intended for testing only. +You should use a server to host your configuration repositories in production. + + +The initial clone of your configuration repository can be quick and efficient if you keep only text files in it. +If you store binary files, especially large ones, you may experience delays on the first request for configuration or encounter out of memory errors in the server. + +
    +Environment Repository +Where should you store the configuration data for the Config Server? +The strategy that governs this behaviour is the EnvironmentRepository, serving Environment objects. +This Environment is a shallow copy of the domain from the Spring Environment (including propertySources as the main feature). +The Environment resources are parametrized by three variables: + + +{application}, which maps to spring.application.name on the client side. + + +{profile}, which maps to spring.profiles.active on the client (comma-separated list). + + +{label}, which is a server side feature labelling a "versioned" set of config files. + + +Repository implementations generally behave like a Spring Boot application, loading configuration files from a spring.config.name equal to the {application} parameter, and spring.profiles.active equal to the {profiles} parameter. +Precedence rules for profiles are also the same as in a regular Spring Boot application: Active profiles take precedence over defaults, and, if there are multiple profiles, the last one wins (similar to adding entries to a Map). +The following sample client application has this bootstrap configuration: + +bootstrap.yml + +spring: + application: + name: foo + profiles: + active: dev,mysql + + +(As usual with a Spring Boot application, these properties could also be set by environment variables or command line arguments). +If the repository is file-based, the server creates an +Environment from application.yml (shared between all clients) and +foo.yml (with foo.yml taking precedence). +If the YAML files have documents inside them that point to Spring profiles, those are applied with higher precedence (in order of the profiles listed). +If there are profile-specific YAML (or properties) files, these are also applied with higher precedence than the defaults. +Higher precedence translates to a PropertySource listed earlier in the Environment. +(These same rules apply in a standalone Spring Boot application.) +You can set spring.cloud.config.server.accept-empty to false so that Server would return a HTTP 404 status, if the application is not found.By default, this flag is set to true. +
    +Git Backend +The default implementation of EnvironmentRepository uses a Git backend, which is very convenient for managing upgrades and physical environments and for auditing changes. +To change the location of the repository, you can set the spring.cloud.config.server.git.uri configuration property in the Config Server (for example in application.yml). +If you set it with a file: prefix, it should work from a local repository so that you can get started quickly and easily without a server. However, in that case, the server operates directly on the local repository without cloning it (it does not matter if it is not bare because the Config Server never makes changes to the "remote" repository). +To scale the Config Server up and make it highly available, you need to have all instances of the server pointing to the same repository, so only a shared file system would work. +Even in that case, it is better to use the ssh: protocol for a shared filesystem repository, so that the server can clone it and use a local working copy as a cache. +This repository implementation maps the {label} parameter of the HTTP resource to a git label (commit id, branch name, or tag). +If the git branch or tag name contains a slash (/), then the label in the HTTP URL should instead be specified with the special string (_) (to avoid ambiguity with other URL paths). +For example, if the label is foo/bar, replacing the slash would result in the following label: foo(_)bar. +The inclusion of the special string (_) can also be applied to the {application} parameter. +If you use a command-line client such as curl, be careful with the brackets in the URL — you should escape them from the shell with single quotes (''). +
    +Skipping SSL Certificate Validation +The configuration server’s validation of the Git server’s SSL certificate can be disabled by setting the git.skipSslValidation property to true (default is false). +spring: + cloud: + config: + server: + git: + uri: https://example.com/my/repo + skipSslValidation: true +
    +
    +Setting HTTP Connection Timeout +You can configure the time, in seconds, that the configuration server will wait to acquire an HTTP connection. Use the git.timeout property. +spring: + cloud: + config: + server: + git: + uri: https://example.com/my/repo + timeout: 4 +
    +
    +Placeholders in Git URI +Spring Cloud Config Server supports a git repository URL with placeholders for the {application} and {profile} (and {label} if you need it, but remember that the label is applied as a git label anyway). +So you can support a one repository per application policy by using a structure similar to the following: +spring: + cloud: + config: + server: + git: + uri: https://github.com/myorg/{application} +You can also support a one repository per profile policy by using a similar pattern but with +{profile}. +Additionally, using the special string "(_)" within your {application} parameters can enable support for multiple +organizations, as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/{application} +where {application} is provided at request time in the following format: organization(_)application. +
    +
    +Pattern Matching and Multiple Repositories +Spring Cloud Config also includes support for more complex requirements with pattern +matching on the application and profile name. +The pattern format is a comma-separated list of {application}/{profile} names with wildcards (note that a pattern beginning with a wildcard may need to be quoted), as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + repos: + simple: https://github.com/simple/config-repo + special: + pattern: special*/dev*,*special*/dev* + uri: https://github.com/special/config-repo + local: + pattern: local* + uri: file:/home/configsvc/config-repo +If {application}/{profile} does not match any of the patterns, it uses the default URI defined under spring.cloud.config.server.git.uri. +In the above example, for the simple repository, the pattern is simple/* (it only matches one application named simple in all profiles). The local repository matches all application names beginning with local in all profiles (the /* suffix is added automatically to any pattern that does not have a profile matcher). + +The one-liner short cut used in the simple example can be used only if the only property to be set is the URI. +If you need to set anything else (credentials, pattern, and so on) you need to use the full form. + +The pattern property in the repo is actually an array, so you can use a YAML array (or [0], [1], etc. suffixes in properties files) to bind to multiple patterns. +You may need to do so if you are going to run apps with multiple profiles, as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + repos: + development: + pattern: + - '*/development' + - '*/staging' + uri: https://github.com/development/config-repo + staging: + pattern: + - '*/qa' + - '*/production' + uri: https://github.com/staging/config-repo + +Spring Cloud guesses that a pattern containing a profile that does not end in * implies that you actually want to match a list of profiles starting with this pattern (so */staging is a shortcut for ["*/staging", "*/staging,*"], and so on). +This is common where, for instance, you need to run applications in the development profile locally but also the cloud profile remotely. + +Every repository can also optionally store config files in sub-directories, and patterns to search for those directories can be specified as searchPaths. +The following example shows a config file at the top level: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + searchPaths: foo,bar* +In the preceding example, the server searches for config files in the top level and in the foo/ sub-directory and also any sub-directory whose name begins with bar. +By default, the server clones remote repositories when configuration +is first requested. +The server can be configured to clone the repositories at startup, as shown in the following top-level example: +spring: + cloud: + config: + server: + git: + uri: https://git/common/config-repo.git + repos: + team-a: + pattern: team-a-* + cloneOnStart: true + uri: https://git/team-a/config-repo.git + team-b: + pattern: team-b-* + cloneOnStart: false + uri: https://git/team-b/config-repo.git + team-c: + pattern: team-c-* + uri: https://git/team-a/config-repo.git +In the preceding example, the server clones team-a’s config-repo on startup, before it +accepts any requests. +All other repositories are not cloned until configuration from the repository is requested. + +Setting a repository to be cloned when the Config Server starts up can help to identify a misconfigured configuration source (such as an invalid repository URI) quickly, while the Config Server is starting up. +With cloneOnStart not enabled for a configuration source, the Config Server may start successfully with a misconfigured or invalid configuration source and not detect an error until an application requests configuration from that configuration source. + +
    +
    +Authentication +To use HTTP basic authentication on the remote repository, add the username and password properties separately (not in the URL), as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + username: trolley + password: strongpassword +If you do not use HTTPS and user credentials, SSH should also work out of the box when you store keys in the default directories (~/.ssh) and the URI points to an SSH location, such as git@github.com:configuration/cloud-configuration. +It is important that an entry for the Git server be present in the ~/.ssh/known_hosts file and that it is in ssh-rsa format. +Other formats (such as ecdsa-sha2-nistp256) are not supported. +To avoid surprises, you should ensure that only one entry is present in the known_hosts file for the Git server and that it matches the URL you provided to the config server. +If you use a hostname in the URL, you want to have exactly that (not the IP) in the known_hosts file. +The repository is accessed by using JGit, so any documentation you find on that should be applicable. +HTTPS proxy settings can be set in ~/.git/config or (in the same way as for any other JVM process) with +system properties (-Dhttps.proxyHost and -Dhttps.proxyPort). + +If you do not know where your ~/.git directory is, use git config --global to manipulate the settings (for example, git config --global http.sslVerify false). + +
    +
    +Authentication with AWS CodeCommit +Spring Cloud Config Server also supports AWS CodeCommit authentication. +AWS CodeCommit uses an authentication helper when using Git from the command line. +This helper is not used with the JGit library, so a JGit CredentialProvider for AWS CodeCommit is created if the Git URI matches the AWS CodeCommit pattern. +AWS CodeCommit URIs follow this pattern://git-codecommit.${AWS_REGION}.amazonaws.com/${repopath}. +If you provide a username and password with an AWS CodeCommit URI, they must be the AWS accessKeyId and secretAccessKey that provide access to the repository. +If you do not specify a username and password, the accessKeyId and secretAccessKey are retrieved by using the AWS Default Credential Provider Chain. +If your Git URI matches the CodeCommit URI pattern (shown earlier), you must provide valid AWS credentials in the username and password or in one of the locations supported by the default credential provider chain. +AWS EC2 instances may use IAM Roles for EC2 Instances. + +The aws-java-sdk-core jar is an optional dependency. +If the aws-java-sdk-core jar is not on your classpath, the AWS Code Commit credential provider is not created, regardless of the git server URI. + +
    +
    +Git SSH configuration using properties +By default, the JGit library used by Spring Cloud Config Server uses SSH configuration files such as ~/.ssh/known_hosts and /etc/ssh/ssh_config when connecting to Git repositories by using an SSH URI. +In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. +For those cases, SSH configuration can be set by using Java properties. +In order to activate property-based SSH configuration, the spring.cloud.config.server.git.ignoreLocalSshSettings property must be set to true, as shown in the following example: + spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo1.git + ignoreLocalSshSettings: true + hostKey: someHostKey + hostKeyAlgorithm: ssh-rsa + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX + IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF + ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud + 1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i + oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W + DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd + fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b + BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG + EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj + 5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8 + +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe + pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG + ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ + xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW + dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi + PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX + VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z + FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R + gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4 + VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV + cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee + KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N + CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs + q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J + 69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT + -----END RSA PRIVATE KEY----- +The following table describes the SSH configuration properties. + +SSH Configuration Properties + + + + + +Property Name +Remarks + + + + +ignoreLocalSshSettings +If true, use property-based instead of file-based SSH config. Must be set at as spring.cloud.config.server.git.ignoreLocalSshSettings, not inside a repository definition. + + +privateKey +Valid SSH private key. Must be set if ignoreLocalSshSettings is true and Git URI is SSH format. + + +hostKey +Valid SSH host key. Must be set if hostKeyAlgorithm is also set. + + +hostKeyAlgorithm +One of ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521. Must be set if hostKey is also set. + + +strictHostKeyChecking +true or false. If false, ignore errors with host key. + + +knownHostsFile +Location of custom .known_hosts file. + + +preferredAuthentications +Override server authentication method order. This should allow for evading login prompts if server has keyboard-interactive authentication before the publickey method. + + + +
    +
    +
    +Placeholders in Git Search Paths +Spring Cloud Config Server also supports a search path with placeholders for the {application} and {profile} (and {label} if +you need it), as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + searchPaths: '{application}' +The preceding listing causes a search of the repository for files in the same name as the directory (as well as the top level). +Wildcards are also valid in a search path with placeholders (any matching directory is included in the search). +
    +
    +Force pull in Git Repositories +As mentioned earlier, Spring Cloud Config Server makes a clone of the remote git repository in case the local copy gets dirty (for example, +folder content changes by an OS process) such that Spring Cloud Config Server cannot update the local copy from remote repository. +To solve this issue, there is a force-pull property that makes Spring Cloud Config Server force pull from the remote repository if the local copy is dirty, as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + force-pull: true +If you have a multiple-repositories configuration, you can configure the force-pull property per repository, as shown in the following example: +spring: + cloud: + config: + server: + git: + uri: https://git/common/config-repo.git + force-pull: true + repos: + team-a: + pattern: team-a-* + uri: https://git/team-a/config-repo.git + force-pull: true + team-b: + pattern: team-b-* + uri: https://git/team-b/config-repo.git + force-pull: true + team-c: + pattern: team-c-* + uri: https://git/team-a/config-repo.git + +The default value for force-pull property is false. + +
    +
    +Deleting untracked branches in Git Repositories +As Spring Cloud Config Server has a clone of the remote git repository +after check-outing branch to local repo (e.g fetching properties by label) it will keep this branch +forever or till the next server restart (which creates new local repo). +So there could be a case when remote branch is deleted but local copy of it is still available for fetching. +And if Spring Cloud Config Server client service starts with --spring.cloud.config.label=deletedRemoteBranch,master +it will fetch properties from deletedRemoteBranch local branch, but not from master. +In order to keep local repository branches clean and up to remote - deleteUntrackedBranches property could be set. +It will make Spring Cloud Config Server force delete untracked branches from local repository. +Example: +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + deleteUntrackedBranches: true + +The default value for deleteUntrackedBranches property is false. + +
    +
    +Git Refresh Rate +You can control how often the config server will fetch updated configuration data +from your Git backend by using spring.cloud.config.server.git.refreshRate. The +value of this property is specified in seconds. By default the value is 0, meaning +the config server will fetch updated configuration from the Git repo every time it +is requested. +
    +
    +
    +Version Control Backend Filesystem Use + +With VCS-based backends (git, svn), files are checked out or cloned to the local filesystem. +By default, they are put in the system temporary directory with a prefix of config-repo-. +On linux, for example, it could be /tmp/config-repo-<randomid>. +Some operating systems routinely clean out temporary directories. +This can lead to unexpected behavior, such as missing properties. +To avoid this problem, change the directory that Config Server uses by setting spring.cloud.config.server.git.basedir or spring.cloud.config.server.svn.basedir to a directory that does not reside in the system temp structure. + +
    +
    +File System Backend +There is also a native profile in the Config Server that does not use Git but loads the config files from the local classpath or file system (any static URL you want to point to with spring.cloud.config.server.native.searchLocations). +To use the native profile, launch the Config Server with spring.profiles.active=native. + +Remember to use the file: prefix for file resources (the default without a prefix is usually the classpath). +As with any Spring Boot configuration, you can embed ${}-style environment placeholders, but remember that absolute paths in Windows require an extra / (for example, file:///${user.home}/config-repo). + + +The default value of the searchLocations is identical to a local Spring Boot application (that is, [classpath:/, classpath:/config, +file:./, file:./config]). +This does not expose the application.properties from the server to all clients, because any property sources present in the server are removed before being sent to the client. + + +A filesystem backend is great for getting started quickly and for testing. +To use it in production, you need to be sure that the file system is reliable and shared across all instances of the Config Server. + +The search locations can contain placeholders for {application}, {profile}, and {label}. +In this way, you can segregate the directories in the path and choose a strategy that makes sense for you (such as subdirectory per application or subdirectory per profile). +If you do not use placeholders in the search locations, this repository also appends the {label} parameter of the HTTP resource to a suffix on the search path, so properties files are loaded from each search location and a subdirectory with the same name as the label (the labelled properties take precedence in the Spring Environment). +Thus, the default behaviour with no placeholders is the same as adding a search location ending with /{label}/. +For example, file:/tmp/config is the same as file:/tmp/config,file:/tmp/config/{label}. +This behavior can be disabled by setting spring.cloud.config.server.native.addLabelLocations=false. +
    +
    +Vault Backend +Spring Cloud Config Server also supports Vault as a backend. + +Vault is a tool for securely accessing secrets. +A secret is anything that to which you want to tightly control access, such as API keys, passwords, certificates, and other sensitive information. Vault provides a unified interface to any secret while providing tight access control and recording a detailed audit log. + +For more information on Vault, see the Vault quick start guide. +To enable the config server to use a Vault backend, you can run your config server with the vault profile. +For example, in your config server’s application.properties, you can add spring.profiles.active=vault. +By default, the config server assumes that your Vault server runs at http://127.0.0.1:8200. +It also assumes that the name of backend is secret and the key is application. +All of these defaults can be configured in your config server’s application.properties. +The following table describes configurable Vault properties: + + + + + + +Name +Default Value + + + + +host +127.0.0.1 + + +port +8200 + + +scheme +http + + +backend +secret + + +defaultKey +application + + +profileSeparator +, + + +kvVersion +1 + + +skipSslValidation +false + + +timeout +5 + + +namespace +null + + + + + +All of the properties in the preceding table must be prefixed with spring.cloud.config.server.vault or placed in the correct Vault section of a composite configuration. + +All configurable properties can be found in org.springframework.cloud.config.server.environment.VaultEnvironmentProperties. +Vault 0.10.0 introduced a versioned key-value backend (k/v backend version 2) that exposes a different API than earlier versions, it now requires a data/ between the mount path and the actual context path and wraps secrets in a data object. Setting kvVersion=2 will take this into account. +Optionally, there is support for the Vault Enterprise X-Vault-Namespace header. To have it sent to Vault set the namespace property. +With your config server running, you can make HTTP requests to the server to retrieve +values from the Vault backend. +To do so, you need a token for your Vault server. +First, place some data in you Vault, as shown in the following example: +$ vault kv put secret/application foo=bar baz=bam +$ vault kv put secret/myapp foo=myappsbar +Second, make an HTTP request to your config server to retrieve the values, as shown in the following example: +$ curl -X "GET" "http://localhost:8888/myapp/default" -H "X-Config-Token: yourtoken" +You should see a response similar to the following: +{ + "name":"myapp", + "profiles":[ + "default" + ], + "label":null, + "version":null, + "state":null, + "propertySources":[ + { + "name":"vault:myapp", + "source":{ + "foo":"myappsbar" + } + }, + { + "name":"vault:application", + "source":{ + "baz":"bam", + "foo":"bar" + } + } + ] +} +
    +Multiple Properties Sources +When using Vault, you can provide your applications with multiple properties sources. +For example, assume you have written data to the following paths in Vault: +secret/myApp,dev +secret/myApp +secret/application,dev +secret/application +Properties written to secret/application are available to all applications using the Config Server. +An application with the name, myApp, would have any properties written to secret/myApp and secret/application available to it. +When myApp has the dev profile enabled, properties written to all of the above paths would be available to it, with properties in the first path in the list taking priority over the others. +
    +
    +
    +Accessing Backends Through a Proxy +The configuration server can access a Git or Vault backend through an HTTP or HTTPS proxy. This behavior is controlled for either Git or Vault by settings under proxy.http and proxy.https. These settings are per repository, so if you are using a composite environment repository you must configure proxy settings for each backend in the composite individually. If using a network which requires separate proxy servers for HTTP and HTTPS URLs, you can configure both the HTTP and the HTTPS proxy settings for a single backend. +The following table describes the proxy configuration properties for both HTTP and HTTPS proxies. All of these properties must be prefixed by proxy.http or proxy.https. + +Proxy Configuration Properties + + + + + +Property Name +Remarks + + + + +host +The host of the proxy. + + +port +The port with which to access the proxy. + + +nonProxyHosts +Any hosts which the configuration server should access outside the proxy. If values are provided for both proxy.http.nonProxyHosts and proxy.https.nonProxyHosts, the proxy.http value will be used. + + +username +The username with which to authenticate to the proxy. If values are provided for both proxy.http.username and proxy.https.username, the proxy.http value will be used. + + +password +The password with which to authenticate to the proxy. If values are provided for both proxy.http.password and proxy.https.password, the proxy.http value will be used. + + + +
    +The following configuration uses an HTTPS proxy to access a Git repository. +spring: + profiles: + active: git + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + proxy: + https: + host: my-proxy.host.io + password: myproxypassword + port: '3128' + username: myproxyusername + nonProxyHosts: example.com +
    +
    +Sharing Configuration With All Applications +Sharing configuration between all applications varies according to which approach you take, as described in the following topics: + + + + + + + + +
    +File Based Repositories +With file-based (git, svn, and native) repositories, resources with file names in application* (application.properties, application.yml, application-*.properties, and so on) are shared between all client applications. +You can use resources with these file names to configure global defaults and have them be overridden by application-specific files as necessary. +The #_property_overrides[property overrides] feature can also be used for setting global defaults, with placeholders applications +allowed to override them locally. + +With the native profile (a local file system backend) , you should use an explicit search location that is not part of the server’s own configuration. +Otherwise, the application* resources in the default search locations get removed because they are part of the server. + +
    +
    +Vault Server +When using Vault as a backend, you can share configuration with all applications by placing configuration in secret/application. +For example, if you run the following Vault command, all applications using the config server will have the properties foo and baz available to them: +$ vault write secret/application foo=bar baz=bam +
    +
    +CredHub Server +When using CredHub as a backend, you can share configuration with all applications by placing configuration in /application/ or by placing it in the default profile for the application. +For example, if you run the following CredHub command, all applications using the config server will have the properties shared.color1 and shared.color2 available to them: +credhub set --name "/application/profile/master/shared" --type=json +value: {"shared.color1": "blue", "shared.color": "red"} +credhub set --name "/my-app/default/master/more-shared" --type=json +value: {"shared.word1": "hello", "shared.word2": "world"} +
    +
    +
    +JDBC Backend +Spring Cloud Config Server supports JDBC (relational database) as a backend for configuration properties. +You can enable this feature by adding spring-jdbc to the classpath and using the jdbc profile or by adding a bean of type JdbcEnvironmentRepository. +If you include the right dependencies on the classpath (see the user guide for more details on that), Spring Boot configures a data source. +The database needs to have a table called PROPERTIES with columns called APPLICATION, PROFILE, and LABEL (with the usual Environment meaning), plus KEY and VALUE for the key and value pairs in Properties style. +All fields are of type String in Java, so you can make them VARCHAR of whatever length you need. +Property values behave in the same way as they would if they came from Spring Boot properties files named {application}-{profile}.properties, including all the encryption and decryption, which will be applied as post-processing steps (that is, not in the repository implementation directly). +
    +
    +CredHub Backend +Spring Cloud Config Server supports CredHub as a backend for configuration properties. +You can enable this feature by adding a dependency to Spring CredHub. + +pom.xml + +<dependencies> + <dependency> + <groupId>org.springframework.credhub</groupId> + <artifactId>spring-credhub-starter</artifactId> + </dependency> +</dependencies> + + +The following configuration uses mutual TLS to access a CredHub: +spring: + profiles: + active: credhub + cloud: + config: + server: + credhub: + url: https://credhub:8844 +The properties should be stored as JSON, such as: +credhub set --name "/demo-app/default/master/toggles" --type=json +value: {"toggle.button": "blue", "toggle.link": "red"} +credhub set --name "/demo-app/default/master/abs" --type=json +value: {"marketing.enabled": true, "external.enabled": false} +All client applications with the name spring.cloud.config.name=demo-app will have the following properties available to them: +{ + toggle.button: "blue", + toggle.link: "red", + marketing.enabled: true, + external.enabled: false +} + +When no profile is specified default will be used and when no label is specified master will be used as a default value. +NOTE: Values added to application will be shared by all the applications. + +
    +OAuth 2.0 +You can authenticate with OAuth 2.0 using UAA as a provider. + +pom.xml + +<dependencies> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-config</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-oauth2-client</artifactId> + </dependency> +</dependencies> + + +The following configuration uses OAuth 2.0 and UAA to access a CredHub: +spring: + profiles: + active: credhub + cloud: + config: + server: + credhub: + url: https://credhub:8844 + oauth2: + registration-id: credhub-client + security: + oauth2: + client: + registration: + credhub-client: + provider: uaa + client-id: credhub_config_server + client-secret: asecret + authorization-grant-type: client_credentials + provider: + uaa: + token-uri: https://uaa:8443/oauth/token + +The used UAA client-id should have credhub.read as scope. + +
    +
    +
    +Composite Environment Repositories +In some scenarios, you may wish to pull configuration data from multiple environment repositories. +To do so, you can enable the composite profile in your configuration server’s application properties or YAML file. +If, for example, you want to pull configuration data from a Subversion repository as well as two Git repositories, you can set the following properties for your configuration server: +spring: + profiles: + active: composite + cloud: + config: + server: + composite: + - + type: svn + uri: file:///path/to/svn/repo + - + type: git + uri: file:///path/to/rex/git/repo + - + type: git + uri: file:///path/to/walter/git/repo +Using this configuration, precedence is determined by the order in which repositories are listed under the composite key. +In the above example, the Subversion repository is listed first, so a value found in the Subversion repository will override values found for the same property in one of the Git repositories. +A value found in the rex Git repository will be used before a value found for the same property in the walter Git repository. +If you want to pull configuration data only from repositories that are each of distinct types, you can enable the corresponding profiles, rather than the composite profile, in your configuration server’s application properties or YAML file. +If, for example, you want to pull configuration data from a single Git repository and a single HashiCorp Vault server, you can set the following properties for your configuration server: +spring: + profiles: + active: git, vault + cloud: + config: + server: + git: + uri: file:///path/to/git/repo + order: 2 + vault: + host: 127.0.0.1 + port: 8200 + order: 1 +Using this configuration, precedence can be determined by an order property. +You can use the order property to specify the priority order for all your repositories. +The lower the numerical value of the order property, the higher priority it has. +The priority order of a repository helps resolve any potential conflicts between repositories that contain values for the same properties. + +If your composite environment includes a Vault server as in the previous example, you must include a Vault token in every request made to the configuration server. See Vault Backend. + + +Any type of failure when retrieving values from an environment repository results in a failure for the entire composite environment. + + +When using a composite environment, it is important that all repositories contain the same labels. +If you have an environment similar to those in the preceding examples and you request configuration data with the master label but the Subversion repository does not contain a branch called master, the entire request fails. + +
    +Custom Composite Environment Repositories +In addition to using one of the environment repositories from Spring Cloud, you can also provide your own EnvironmentRepository bean to be included as part of a composite environment. +To do so, your bean must implement the EnvironmentRepository interface. +If you want to control the priority of your custom EnvironmentRepository within the composite environment, you should also implement the Ordered interface and override the getOrdered method. +If you do not implement the Ordered interface, your EnvironmentRepository is given the lowest priority. +
    +
    +
    +Property Overrides +The Config Server has an overrides feature that lets the operator provide configuration properties to all applications. +The overridden properties cannot be accidentally changed by the application with the normal Spring Boot hooks. +To declare overrides, add a map of name-value pairs to spring.cloud.config.server.overrides, as shown in the following example: +spring: + cloud: + config: + server: + overrides: + foo: bar +The preceding examples causes all applications that are config clients to read foo=bar, independent of their own configuration. + +A configuration system cannot force an application to use configuration data in any particular way. +Consequently, overrides are not enforceable. +However, they do provide useful default behavior for Spring Cloud Config clients. + + +Normally, Spring environment placeholders with ${} can be escaped (and resolved on the client) by using backslash (\) to escape the $ or the {. +For example, \${app.foo:bar} resolves to bar, unless the app provides its own app.foo. + + +In YAML, you do not need to escape the backslash itself. +However, in properties files, you do need to escape the backslash, when you configure the overrides on the server. + +You can change the priority of all overrides in the client to be more like default values, letting applications supply their own values in environment variables or System properties, by setting the spring.cloud.config.overrideNone=true flag (the default is false) in the remote repository. +
    +
    +
    +Health Indicator +Config Server comes with a Health Indicator that checks whether the configured EnvironmentRepository is working. +By default, it asks the EnvironmentRepository for an application named app, the default profile, and the default label provided by the EnvironmentRepository implementation. +You can configure the Health Indicator to check more applications along with custom profiles and custom labels, as shown in the following example: +spring: + cloud: + config: + server: + health: + repositories: + myservice: + label: mylabel + myservice-dev: + name: myservice + profiles: development +You can disable the Health Indicator by setting spring.cloud.config.server.health.enabled=false. +
    +
    +Security +You can secure your Config Server in any way that makes sense to you (from physical network security to OAuth2 bearer tokens), because Spring Security and Spring Boot offer support for many security arrangements. +To use the default Spring Boot-configured HTTP Basic security, include Spring Security on the classpath (for example, through spring-boot-starter-security). +The default is a username of user and a randomly generated password. A random password is not useful in practice, so we recommend you configure the password (by setting spring.security.user.password) and encrypt it (see below for instructions on how to do that). +
    +
    +Encryption and Decryption + +To use the encryption and decryption features you need the full-strength JCE installed in your JVM (it is not included by default). +You can download the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files from Oracle and follow the installation instructions (essentially, you need to replace the two policy files in the JRE lib/security directory with the ones that you downloaded). + +If the remote property sources contain encrypted content (values starting with {cipher}), they are decrypted before sending to clients over HTTP. +The main advantage of this setup is that the property values need not be in plain text when they are at rest (for example, in a git repository). +If a value cannot be decrypted, it is removed from the property source and an additional property is added with the same key but prefixed with invalid and a value that means not applicable (usually <n/a>). +This is largely to prevent cipher text being used as a password and accidentally leaking. +If you set up a remote config repository for config client applications, it might contain an application.yml similar to the following: + +application.yml + +spring: + datasource: + username: dbuser + password: '{cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ' + + +Encrypted values in a .properties file must not be wrapped in quotes. Otherwise, the value is not decrypted. The following example shows values that would work: + +application.properties + +spring.datasource.username: dbuser +spring.datasource.password: {cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ + + +You can safely push this plain text to a shared git repository, and the secret password remains protected. +The server also exposes /encrypt and /decrypt endpoints (on the assumption that these are secured and only accessed by authorized agents). +If you edit a remote config file, you can use the Config Server to encrypt values by POSTing to the /encrypt endpoint, as shown in the following example: +$ curl localhost:8888/encrypt -d mysecret +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda + +If the value you encrypt has characters in it that need to be URL encoded, you should use the --data-urlencode option to curl to make sure they are encoded properly. + + +Be sure not to include any of the curl command statistics in the encrypted value. +Outputting the value to a file can help avoid this problem. + +The inverse operation is also available through /decrypt (provided the server is +configured with a symmetric key or a full key pair), as shown in the following example: +$ curl localhost:8888/decrypt -d 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +mysecret + +If you testing with curl, then use --data-urlencode (instead of -d) or set an explicit Content-Type: text/plain to make sure curl encodes the data correctly when there are special characters ('+' is particularly tricky). + +Take the encrypted value and add the {cipher} prefix before you put it in the YAML or properties file and before you commit and push it to a remote (potentially insecure) store. +The /encrypt and /decrypt endpoints also both accept paths in the form of /*/{application}/{profiles}, which can be used to control cryptography on a per-application (name) and per-profile basis when clients call into the main environment resource. + +To control the cryptography in this granular way, you must also provide a @Bean of type TextEncryptorLocator that creates a different encryptor per name and profiles. +The one that is provided by default does not do so (all encryptions use the same key). + +The spring command line client (with Spring Cloud CLI extensions +installed) can also be used to encrypt and decrypt, as shown in the following example: +$ spring encrypt mysecret --key foo +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +$ spring decrypt --key foo 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +mysecret +To use a key in a file (such as an RSA public key for encryption), prepend +the key value with "@" and provide the file path, as shown in the following example: +$ spring encrypt mysecret --key @${HOME}/.ssh/id_rsa.pub +AQAjPgt3eFZQXwt8tsHAVv/QHiY5sI2dRcR+... + +The --key argument is mandatory (despite having a -- prefix). + +
    +
    +Key Management +The Config Server can use a symmetric (shared) key or an asymmetric one (RSA key pair). +The asymmetric choice is superior in terms of security, but it is often more convenient to use a symmetric key since it is a single property value to configure in the bootstrap.properties. +To configure a symmetric key, you need to set encrypt.key to a secret String (or use the ENCRYPT_KEY environment variable to keep it out of plain-text configuration files). + +You cannot configure an asymmetric key using encrypt.key. + +To configure an asymmetric key use a keystore (e.g. as +created by the keytool utility that comes with the JDK). The +keystore properties are encrypt.keyStore.* with * equal to + + + + + + +Property +Description + + + + +encrypt.keyStore.location +Contains a Resource location + + +encrypt.keyStore.password +Holds the password that unlocks the keystore + + +encrypt.keyStore.alias +Identifies which key in the store to use + + +encrypt.keyStore.type +The type of KeyStore to create. Defaults to jks. + + + + +The encryption is done with the public key, and a private key is +needed for decryption. +Thus, in principle, you can configure only the public key in the server if you want to only encrypt (and are prepared to decrypt the values yourself locally with the private key). +In practice, you might not want to do decrypt locally, because it spreads the key management process around all the clients, instead of +concentrating it in the server. +On the other hand, it can be a useful option if your config server is relatively insecure and only a handful of clients need the encrypted properties. +
    +
    +Creating a Key Store for Testing +To create a keystore for testing, you can use a command resembling the following: +$ keytool -genkeypair -alias mytestkey -keyalg RSA \ + -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" \ + -keypass changeme -keystore server.jks -storepass letmein + +When using JDK 11 or above you may get the following warning when using the command above. In this case +you probably want to make sure the keypass and storepass values match. + +Warning: Different store and key passwords not supported for PKCS12 KeyStores. Ignoring user-specified -keypass value. +Put the server.jks file in the classpath (for instance) and then, in +your bootstrap.yml, for the Config Server, create the following settings: +encrypt: + keyStore: + location: classpath:/server.jks + password: letmein + alias: mytestkey + secret: changeme +
    +
    +Using Multiple Keys and Key Rotation +In addition to the {cipher} prefix in encrypted property values, the Config Server looks for zero or more {name:value} prefixes before the start of the (Base64 encoded) cipher text. +The keys are passed to a TextEncryptorLocator, which can do whatever logic it needs to locate a TextEncryptor for the cipher. +If you have configured a keystore (encrypt.keystore.location), the default locator looks for keys with aliases supplied by the key prefix, with a cipher text like resembling the following: +foo: + bar: `{cipher}{key:testkey}...` +The locator looks for a key named "testkey". +A secret can also be supplied by using a {secret:…​} value in the prefix. +However, if it is not supplied, the default is to use the keystore password (which is what you get when you build a keystore and do not specify a secret). +If you do supply a secret, you should also encrypt the secret using a custom SecretLocator. +When the keys are being used only to encrypt a few bytes of configuration data (that is, they are not being used elsewhere), key rotation is hardly ever necessary on cryptographic grounds. +However, you might occasionally need to change the keys (for example, in the event of a security breach). +In that case, all the clients would need to change their source config files (for example, in git) and use a new {key:…​} prefix in all the ciphers. +Note that the clients need to first check that the key alias is available in the Config Server keystore. + +If you want to let the Config Server handle all encryption as well as decryption, the {name:value} prefixes can also be added as plain text posted to the /encrypt endpoint, . + +
    +
    +Serving Encrypted Properties +Sometimes you want the clients to decrypt the configuration locally, instead of doing it in the server. +In that case, if you provide the encrypt.* configuration to locate a key, you can still have /encrypt and /decrypt endpoints, but you need to explicitly switch off the decryption of outgoing properties by placing spring.cloud.config.server.encrypt.enabled=false in bootstrap.[yml|properties]. +If you do not care about the endpoints, it should work if you do not configure either the key or the enabled flag. +
    +
    + +Serving Alternative Formats +The default JSON format from the environment endpoints is perfect for consumption by Spring applications, because it maps directly onto the Environment abstraction. +If you prefer, you can consume the same data as YAML or Java properties by adding a suffix (".yml", ".yaml" or ".properties") to the resource path. +This can be useful for consumption by applications that do not care about the structure of the JSON endpoints or the extra metadata they provide (for example, an application that is not using Spring might benefit from the simplicity of this approach). +The YAML and properties representations have an additional flag (provided as a boolean query parameter called resolvePlaceholders) to signal that placeholders in the source documents (in the standard Spring ${…​} form) should be resolved in the output before rendering, where possible. +This is a useful feature for consumers that do not know about the Spring placeholder conventions. + +There are limitations in using the YAML or properties formats, mainly in relation to the loss of metadata. +For example, the JSON is structured as an ordered list of property sources, with names that correlate with the source. +The YAML and properties forms are coalesced into a single map, even if the origin of the values has multiple sources, and the names of the original source files are lost. +Also, the YAML representation is not necessarily a faithful representation of the YAML source in a backing repository either. It is constructed from a list of flat property sources, and assumptions have to be made about the form of the keys. + + + +Serving Plain Text +Instead of using the Environment abstraction (or one of the alternative representations of it in YAML or properties format), your applications might need generic plain-text configuration files that are tailored to their environment. +The Config Server provides these through an additional endpoint at /{application}/{profile}/{label}/{path}, where application, profile, and label have the same meaning as the regular environment endpoint, but path is a path to a file name (such as log.xml). +The source files for this endpoint are located in the same way as for the environment endpoints. +The same search path is used for properties and YAML files. +However, instead of aggregating all matching resources, only the first one to match is returned. +After a resource is located, placeholders in the normal format (${…​}) are resolved by using the effective Environment for the supplied application name, profile, and label. +In this way, the resource endpoint is tightly integrated with the environment endpoints. +Consider the following example for a GIT or SVN repository: +application.yml +nginx.conf +where nginx.conf looks like this: +server { + listen 80; + server_name ${nginx.server.name}; +} +and application.yml like this: +nginx: + server: + name: example.com +--- +spring: + profiles: development +nginx: + server: + name: develop.com +The /foo/default/master/nginx.conf resource might be as follows: +server { + listen 80; + server_name example.com; +} +and /foo/development/master/nginx.conf like this: +server { + listen 80; + server_name develop.com; +} + +As with the source files for environment configuration, the profile is used to resolve the file name. +So, if you want a profile-specific file, /*/development/*/logback.xml can be resolved by a file called logback-development.xml (in preference to logback.xml). + + +If you do not want to supply the label and let the server use the default label, you can supply a useDefaultLabel request parameter. +So, the preceding example for the default profile could be /foo/default/nginx.conf?useDefaultLabel. + + + +Embedding the Config Server +The Config Server runs best as a standalone application. +However, if need be, you can embed it in another application. +To do so, use the @EnableConfigServer annotation. +An optional property named spring.cloud.config.server.bootstrap can be useful in this case. +It is a flag to indicate whether the server should configure itself from its own remote repository. +By default, the flag is off, because it can delay startup. +However, when embedded in another application, it makes sense to initialize the same way as any other application. +When setting spring.cloud.config.server.bootstrap to true you must also use a composite environment repository configuration. +For example +spring: + application: + name: configserver + profiles: + active: composite + cloud: + config: + server: + composite: + - type: native + search-locations: ${HOME}/Desktop/config + bootstrap: true + +If you use the bootstrap flag, the config server needs to have its name and repository URI configured in bootstrap.yml. + +To change the location of the server endpoints, you can (optionally) set spring.cloud.config.server.prefix (for example, /config), to serve the resources under a prefix. +The prefix should start but not end with a /. +It is applied to the @RequestMappings in the Config Server (that is, underneath the Spring Boot server.servletPath and server.contextPath prefixes). +If you want to read the configuration for an application directly from the backend repository (instead of from the config server), you +basically want an embedded config server with no endpoints. +You can switch off the endpoints entirely by not using the @EnableConfigServer annotation (set spring.cloud.config.server.bootstrap=true). + + +Push Notifications and Spring Cloud Bus +Many source code repository providers (such as Github, Gitlab, Gitea, Gitee, Gogs, or Bitbucket) notify you of changes in a repository through a webhook. +You can configure the webhook through the provider’s user interface as a URL and a set of events in which you are interested. +For instance, Github uses a POST to the webhook with a JSON body containing a list of commits and a header (X-Github-Event) set to push. +If you add a dependency on the spring-cloud-config-monitor library and activate the Spring Cloud Bus in your Config Server, then a /monitor endpoint is enabled. +When the webhook is activated, the Config Server sends a RefreshRemoteApplicationEvent targeted at the applications it thinks might have changed. +The change detection can be strategized. +However, by default, it looks for changes in files that match the application name (for example, foo.properties is targeted at the foo application, while application.properties is targeted at all applications). +The strategy to use when you want to override the behavior is PropertyPathNotificationExtractor, which accepts the request headers and body as parameters and returns a list of file paths that changed. +The default configuration works out of the box with Github, Gitlab, Gitea, Gitee, Gogs or Bitbucket. +In addition to the JSON notifications from Github, Gitlab, Gitee, or Bitbucket, you can trigger a change notification by POSTing to /monitor with form-encoded body parameters in the pattern of path={application}. +Doing so broadcasts to applications matching the {application} pattern (which can contain wildcards). + +The RefreshRemoteApplicationEvent is transmitted only if the spring-cloud-bus is activated in both the Config Server and in the client application. + + +The default configuration also detects filesystem changes in local git repositories. In that case, the webhook is not used. However, as soon as you edit a config file, a refresh is broadcast. + + + +Spring Cloud Config Client +A Spring Boot application can take immediate advantage of the Spring Config Server (or other external property sources provided by the application developer). +It also picks up some additional useful features related to Environment change events. +
    +Config First Bootstrap +The default behavior for any application that has the Spring Cloud Config Client on the classpath is as follows: +When a config client starts, it binds to the Config Server (through the spring.cloud.config.uri bootstrap configuration property) and initializes Spring Environment with remote property sources. +The net result of this behavior is that all client applications that want to consume the Config Server need a bootstrap.yml (or an environment variable) with the server address set in spring.cloud.config.uri (it defaults to "http://localhost:8888"). +
    +
    +Discovery First Bootstrap +If you use a DiscoveryClient implementation, such as Spring Cloud Netflix and Eureka Service Discovery or Spring Cloud Consul, you can have the Config Server register with the Discovery Service. +However, in the default Config First mode, clients cannot take advantage of the registration. +If you prefer to use DiscoveryClient to locate the Config Server, you can do so by setting spring.cloud.config.discovery.enabled=true (the default is false). +The net result of doing so is that client applications all need a bootstrap.yml (or an environment variable) with the appropriate discovery configuration. +For example, with Spring Cloud Netflix, you need to define the Eureka server address (for example, in eureka.client.serviceUrl.defaultZone). +The price for using this option is an extra network round trip on startup, to locate the service registration. +The benefit is that, as long as the Discovery Service is a fixed point, the Config Server can change its coordinates. +The default service ID is configserver, but you can change that on the client by setting spring.cloud.config.discovery.serviceId (and on the server, in the usual way for a service, such as by setting spring.application.name). +The discovery client implementations all support some kind of metadata map (for example, we have eureka.instance.metadataMap for Eureka). +Some additional properties of the Config Server may need to be configured in its service registration metadata so that clients can connect correctly. +If the Config Server is secured with HTTP Basic, you can configure the credentials as user and password. +Also, if the Config Server has a context path, you can set configPath. +For example, the following YAML file is for a Config Server that is a Eureka client: + +bootstrap.yml + +eureka: + instance: + ... + metadataMap: + user: osufhalskjrtl + password: lviuhlszvaorhvlo5847 + configPath: /config + + +
    +
    +Config Client Fail Fast +In some cases, you may want to fail startup of a service if it cannot connect to the Config Server. +If this is the desired behavior, set the bootstrap configuration property spring.cloud.config.fail-fast=true to make the client halt with an Exception. +
    +
    +Config Client Retry +If you expect that the config server may occasionally be unavailable when your application starts, you can make it keep trying after a failure. +First, you need to set spring.cloud.config.fail-fast=true. +Then you need to add spring-retry and spring-boot-starter-aop to your classpath. +The default behavior is to retry six times with an initial backoff interval of 1000ms and an exponential multiplier of 1.1 for subsequent backoffs. +You can configure these properties (and others) by setting the spring.cloud.config.retry.* configuration properties. + +To take full control of the retry behavior, add a @Bean of type RetryOperationsInterceptor with an ID of configServerRetryInterceptor. +Spring Retry has a RetryInterceptorBuilder that supports creating one. + +
    +
    +Locating Remote Configuration Resources +The Config Service serves property sources from /{application}/{profile}/{label}, where the default bindings in the client app are as follows: + + +"name" = ${spring.application.name} + + +"profile" = ${spring.profiles.active} (actually Environment.getActiveProfiles()) + + +"label" = "master" + + + +When setting the property ${spring.application.name} do not prefix your app name with the reserved word application- to prevent issues resolving the correct property source. + +You can override all of them by setting spring.cloud.config.* (where * is name, profile or label). +The label is useful for rolling back to previous versions of configuration. +With the default Config Server implementation, it can be a git label, branch name, or commit ID. +Label can also be provided as a comma-separated list. +In that case, the items in the list are tried one by one until one succeeds. +This behavior can be useful when working on a feature branch. +For instance, you might want to align the config label with your branch but make it optional (in that case, use spring.cloud.config.label=myfeature,develop). +
    +
    +Specifying Multiple Urls for the Config Server +To ensure high availability when you have multiple instances of Config Server deployed and expect one or more instances to be unavailable from time to time, you can either specify multiple URLs (as a comma-separated list under the spring.cloud.config.uri property) or have all your instances register in a Service Registry like Eureka ( if using Discovery-First Bootstrap mode ). Note that doing so ensures high availability only when the Config Server is not running (that is, when the application has exited) or when a connection timeout has occurred. For example, if the Config Server returns a 500 (Internal Server Error) response or the Config Client receives a 401 from the Config Server (due to bad credentials or other causes), the Config Client does not try to fetch properties from other URLs. An error of that kind indicates a user issue rather than an availability problem. +If you use HTTP basic security on your Config Server, it is currently possible to support per-Config Server auth credentials only if you embed the credentials in each URL you specify under the spring.cloud.config.uri property. If you use any other kind of security mechanism, you cannot (currently) support per-Config Server authentication and authorization. +
    +
    +Configuring Timeouts +If you want to configure timeout thresholds: + + +Read timeouts can be configured by using the property spring.cloud.config.request-read-timeout. + + +Connection timeouts can be configured by using the property spring.cloud.config.request-connect-timeout. + + +
    +
    +Security +If you use HTTP Basic security on the server, clients need to know the password (and username if it is not the default). +You can specify the username and password through the config server URI or via separate username and password properties, as shown in the following example: + +bootstrap.yml + +spring: + cloud: + config: + uri: https://user:secret@myconfig.mycompany.com + + +The following example shows an alternate way to pass the same information: + +bootstrap.yml + +spring: + cloud: + config: + uri: https://myconfig.mycompany.com + username: user + password: secret + + +The spring.cloud.config.password and spring.cloud.config.username values override anything that is provided in the URI. +If you deploy your apps on Cloud Foundry, the best way to provide the password is through service credentials (such as in the URI, since it does not need to be in a config file). +The following example works locally and for a user-provided service on Cloud Foundry named configserver: + +bootstrap.yml + +spring: + cloud: + config: + uri: ${vcap.services.configserver.credentials.uri:http://user:password@localhost:8888} + + +If you use another form of security, you might need to provide a RestTemplate to the ConfigServicePropertySourceLocator (for example, by grabbing it in the bootstrap context and injecting it). +
    +Health Indicator +The Config Client supplies a Spring Boot Health Indicator that attempts to load configuration from the Config Server. +The health indicator can be disabled by setting health.config.enabled=false. +The response is also cached for performance reasons. +The default cache time to live is 5 minutes. +To change that value, set the health.config.time-to-live property (in milliseconds). +
    +
    +Providing A Custom RestTemplate +In some cases, you might need to customize the requests made to the config server from the client. +Typically, doing so involves passing special Authorization headers to authenticate requests to the server. +To provide a custom RestTemplate: + + +Create a new configuration bean with an implementation of PropertySourceLocator, as shown in the following example: + + + +CustomConfigServiceBootstrapConfiguration.java + +@Configuration +public class CustomConfigServiceBootstrapConfiguration { + @Bean + public ConfigServicePropertySourceLocator configServicePropertySourceLocator() { + ConfigClientProperties clientProperties = configClientProperties(); + ConfigServicePropertySourceLocator configServicePropertySourceLocator = new ConfigServicePropertySourceLocator(clientProperties); + configServicePropertySourceLocator.setRestTemplate(customRestTemplate(clientProperties)); + return configServicePropertySourceLocator; + } +} + + + + +In resources/META-INF, create a file called +spring.factories and specify your custom configuration, as shown in the following example: + + + +spring.factories + +org.springframework.cloud.bootstrap.BootstrapConfiguration = com.my.config.client.CustomConfigServiceBootstrapConfiguration + + +
    +
    +Vault +When using Vault as a backend to your config server, the client needs to supply a token for the server to retrieve values from Vault. +This token can be provided within the client by setting spring.cloud.config.token +in bootstrap.yml, as shown in the following example: + +bootstrap.yml + +spring: + cloud: + config: + token: YourVaultToken + + +
    +
    +
    +Nested Keys In Vault +Vault supports the ability to nest keys in a value stored in Vault, as shown in the following example: +echo -n '{"appA": {"secret": "appAsecret"}, "bar": "baz"}' | vault write secret/myapp - +This command writes a JSON object to your Vault. +To access these values in Spring, you would use the traditional dot(.) annotation, as shown in the following example +@Value("${appA.secret}") +String name = "World"; +The preceding code would sets the value of the name variable to appAsecret. +
    +
    +
    + +Spring Cloud Netflix + +Greenwich.SR5 +This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms. With a few +simple annotations you can quickly enable and configure the common patterns inside your +application and build large distributed systems with battle-tested Netflix components. The +patterns provided include Service Discovery (Eureka), Circuit Breaker (Hystrix), +Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon). + + +Service Discovery: Eureka Clients +Service Discovery is one of the key tenets of a microservice-based architecture. +Trying to hand-configure each client or some form of convention can be difficult to do and can be brittle. +Eureka is the Netflix Service Discovery Server and Client. +The server can be configured and deployed to be highly available, with each server replicating state about the registered services to the others. +
    +How to Include Eureka Client +To include the Eureka Client in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-eureka-client. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +
    +
    +Registering with Eureka +When a client registers with Eureka, it provides meta-data about itself — such as host, port, health indicator URL, home page, and other details. +Eureka receives heartbeat messages from each instance belonging to a service. +If the heartbeat fails over a configurable timetable, the instance is normally removed from the registry. +The following example shows a minimal Eureka client application: +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} +Note that the preceding example shows a normal Spring Boot application. +By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server. Configuration is required to locate the Eureka server, as shown in the following example: + +application.yml + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + + +In the preceding example, defaultZone is a magic string fallback value that provides the service URL for any client that does not express a preference (in other words, it is a useful default). + +The defaultZone property is case sensitive and requires camel case because the serviceUrl property is a Map<String, String>. Therefore, the defaultZone property does not follow the normal Spring Boot snake-case convention of default-zone. + +The default application name (that is, the service ID), virtual host, and non-secure port (taken from the Environment) are ${spring.application.name}, ${spring.application.name} and ${server.port}, respectively. +Having spring-cloud-starter-netflix-eureka-client on the classpath makes the app into both a Eureka instance (that is, it registers itself) and a client (it can query the registry to locate other services). +The instance behaviour is driven by eureka.instance.* configuration keys, but the defaults are fine if you ensure that your application has a value for spring.application.name (this is the default for the Eureka service ID or VIP). +See EurekaInstanceConfigBean and EurekaClientConfigBean for more details on the configurable options. +To disable the Eureka Discovery Client, you can set eureka.client.enabled to false. Eureka Discovery Client will also be disabled when spring.cloud.discovery.enabled is set to false. +
    +
    +Authenticating with the Eureka Server +HTTP basic authentication is automatically added to your eureka client if one of the eureka.client.serviceUrl.defaultZone URLs has credentials embedded in it (curl style, as follows: http://user:password@localhost:8761/eureka). +For more complex needs, you can create a @Bean of type DiscoveryClientOptionalArgs and inject ClientFilter instances into it, all of which is applied to the calls from the client to the server. + +Because of a limitation in Eureka, it is not possible to support per-server basic auth credentials, so only the first set that are found is used. + +
    +
    +Status Page and Health Indicator +The status page and health indicators for a Eureka instance default to /info and /health respectively, which are the default locations of useful endpoints in a Spring Boot Actuator application. +You need to change these, even for an Actuator application if you use a non-default context path or servlet path (such as server.servletPath=/custom). The following example shows the default values for the two settings: + +application.yml + +eureka: + instance: + statusPageUrlPath: ${server.servletPath}/info + healthCheckUrlPath: ${server.servletPath}/health + + +These links show up in the metadata that is consumed by clients and are used in some scenarios to decide whether to send requests to your application, so it is helpful if they are accurate. + +In Dalston it was also required to set the status and health check URLs when changing +that management context path. This requirement was removed beginning in Edgware. + +
    +
    +Registering a Secure Application +If your app wants to be contacted over HTTPS, you can set two flags in the EurekaInstanceConfig: + + +eureka.instance.[nonSecurePortEnabled]=[false] + + +eureka.instance.[securePortEnabled]=[true] + + +Doing so makes Eureka publish instance information that shows an explicit preference for secure communication. +The Spring Cloud DiscoveryClient always returns a URI starting with https for a service configured this way. +Similarly, when a service is configured this way, the Eureka (native) instance information has a secure health check URL. +Because of the way Eureka works internally, it still publishes a non-secure URL for the status and home pages unless you also override those explicitly. +You can use placeholders to configure the eureka instance URLs, as shown in the following example: + +application.yml + +eureka: + instance: + statusPageUrl: https://${eureka.hostname}/info + healthCheckUrl: https://${eureka.hostname}/health + homePageUrl: https://${eureka.hostname}/ + + +(Note that ${eureka.hostname} is a native placeholder only available +in later versions of Eureka. You could achieve the same thing with +Spring placeholders as well — for example, by using ${eureka.instance.hostName}.) + +If your application runs behind a proxy, and the SSL termination is in the proxy (for example, if you run in Cloud Foundry or other platforms as a service), then you need to ensure that the proxy forwarded headers are intercepted and handled by the application. +If the Tomcat container embedded in a Spring Boot application has explicit configuration for the 'X-Forwarded-\*` headers, this happens automatically. +The links rendered by your app to itself being wrong (the wrong host, port, or protocol) is a sign that you got this configuration wrong. + +
    +
    +Eureka’s Health Checks +By default, Eureka uses the client heartbeat to determine if a client is up. +Unless specified otherwise, the Discovery Client does not propagate the current health check status of the application, per the Spring Boot Actuator. +Consequently, after successful registration, Eureka always announces that the application is in 'UP' state. This behavior can be altered by enabling Eureka health checks, which results in propagating application status to Eureka. +As a consequence, every other application does not send traffic to applications in states other then 'UP'. +The following example shows how to enable health checks for the client: + +application.yml + +eureka: + client: + healthcheck: + enabled: true + + + +eureka.client.healthcheck.enabled=true should only be set in application.yml. Setting the value in bootstrap.yml causes undesirable side effects, such as registering in Eureka with an UNKNOWN status. + +If you require more control over the health checks, consider implementing your own com.netflix.appinfo.HealthCheckHandler. +
    +
    +Eureka Metadata for Instances and Clients +It is worth spending a bit of time understanding how the Eureka metadata works, so you can use it in a way that makes sense in your platform. +There is standard metadata for information such as hostname, IP address, port numbers, the status page, and health check. +These are published in the service registry and used by clients to contact the services in a straightforward way. +Additional metadata can be added to the instance registration in the eureka.instance.metadataMap, and this metadata is accessible in the remote clients. +In general, additional metadata does not change the behavior of the client, unless the client is made aware of the meaning of the metadata. +There are a couple of special cases, described later in this document, where Spring Cloud already assigns meaning to the metadata map. +
    +Using Eureka on Cloud Foundry +Cloud Foundry has a global router so that all instances of the same app have the same hostname (other PaaS solutions with a similar architecture have the same arrangement). +This is not necessarily a barrier to using Eureka. +However, if you use the router (recommended or even mandatory, depending on the way your platform was set up), you need to explicitly set the hostname and port numbers (secure or non-secure) so that they use the router. +You might also want to use instance metadata so that you can distinguish between the instances on the client (for example, in a custom load balancer). +By default, the eureka.instance.instanceId is vcap.application.instance_id, as shown in the following example: + +application.yml + +eureka: + instance: + hostname: ${vcap.application.uris[0]} + nonSecurePort: 80 + + +Depending on the way the security rules are set up in your Cloud Foundry instance, you might be able to register and use the IP address of the host VM for direct service-to-service calls. +This feature is not yet available on Pivotal Web Services (PWS). +
    +
    +Using Eureka on AWS +If the application is planned to be deployed to an AWS cloud, the Eureka instance must be configured to be AWS-aware. You can do so by customizing the EurekaInstanceConfigBean as follows: +@Bean +@Profile("!default") +public EurekaInstanceConfigBean eurekaInstanceConfig(InetUtils inetUtils) { + EurekaInstanceConfigBean b = new EurekaInstanceConfigBean(inetUtils); + AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka"); + b.setDataCenterInfo(info); + return b; +} +
    +
    +Changing the Eureka Instance ID +A vanilla Netflix Eureka instance is registered with an ID that is equal to its host name (that is, there is only one service per host). +Spring Cloud Eureka provides a sensible default, which is defined as follows: +${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}} +An example is myhost:myappname:8080. +By using Spring Cloud, you can override this value by providing a unique identifier in eureka.instance.instanceId, as shown in the following example: + +application.yml + +eureka: + instance: + instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}} + + +With the metadata shown in the preceding example and multiple service instances deployed on localhost, the random value is inserted there to make the instance unique. +In Cloud Foundry, the vcap.application.instance_id is populated automatically in a Spring Boot application, so the random value is not needed. +
    +
    +
    +Using the EurekaClient +Once you have an application that is a discovery client, you can use it to discover service instances from the Eureka Server. +One way to do so is to use the native com.netflix.discovery.EurekaClient (as opposed to the Spring Cloud DiscoveryClient), as shown in the following example: +@Autowired +private EurekaClient discoveryClient; + +public String serviceUrl() { + InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false); + return instance.getHomePageUrl(); +} + +Do not use the EurekaClient in a @PostConstruct method or in a @Scheduled method (or anywhere where the ApplicationContext might not be started yet). +It is initialized in a SmartLifecycle (with phase=0), so the earliest you can rely on it being available is in another SmartLifecycle with a higher phase. + +
    +EurekaClient without Jersey +By default, EurekaClient uses Jersey for HTTP communication. +If you wish to avoid dependencies from Jersey, you can exclude it from your dependencies. +Spring Cloud auto-configures a transport client based on Spring RestTemplate. +The following example shows Jersey being excluded: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> + <exclusions> + <exclusion> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-client</artifactId> + </exclusion> + <exclusion> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-core</artifactId> + </exclusion> + <exclusion> + <groupId>com.sun.jersey.contribs</groupId> + <artifactId>jersey-apache-client4</artifactId> + </exclusion> + </exclusions> +</dependency> +
    +
    +
    +Alternatives to the Native Netflix EurekaClient +You need not use the raw Netflix EurekaClient. +Also, it is usually more convenient to use it behind a wrapper of some sort. +Spring Cloud has support for Feign (a REST client builder) and Spring RestTemplate through the logical Eureka service identifiers (VIPs) instead of physical URLs. +To configure Ribbon with a fixed list of physical servers, you can set <client>.ribbon.listOfServers to a comma-separated list of physical addresses (or hostnames), where <client> is the ID of the client. +You can also use the org.springframework.cloud.client.discovery.DiscoveryClient, which provides a simple API (not specific to Netflix) for discovery clients, as shown in the following example: +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List<ServiceInstance> list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri(); + } + return null; +} +
    +
    +Why Is It so Slow to Register a Service? +Being an instance also involves a periodic heartbeat to the registry +(through the client’s serviceUrl) with a default duration of 30 seconds. +A service is not available for discovery by clients until the instance, the server, and the client all have the same metadata in their local +cache (so it could take 3 heartbeats). +You can change the period by setting eureka.instance.leaseRenewalIntervalInSeconds. +Setting it to a value of less than 30 speeds up the process of getting clients connected to other services. +In production, it is probably better to stick with the default, because of internal computations in the server that make assumptions about the lease renewal period. +
    +
    +Zones +If you have deployed Eureka clients to multiple zones, you may prefer that those clients use services within the same zone before trying services in another zone. +To set that up, you need to configure your Eureka clients correctly. +First, you need to make sure you have Eureka servers deployed to each zone and that +they are peers of each other. +See the section on zones and regions +for more information. +Next, you need to tell Eureka which zone your service is in. +You can do so by using the metadataMap property. +For example, if service 1 is deployed to both zone 1 and zone 2, you need to set the following Eureka properties in service 1: +Service 1 in Zone 1 +eureka.instance.metadataMap.zone = zone1 +eureka.client.preferSameZoneEureka = true +Service 1 in Zone 2 +eureka.instance.metadataMap.zone = zone2 +eureka.client.preferSameZoneEureka = true +
    +
    +Refreshing Eureka Clients +By default, the EurekaClient bean is refreshable, meaning the Eureka client properties can be changed and refreshed. +When a refresh occurs clients will be unregistered from the Eureka server and there might be a brief moment of time +where all instance of a given service are not available. One way to eliminate this from happening is to disable +the ability to refresh Eureka clients. To do this set eureka.client.refresh.enable=false. +
    +
    + +Service Discovery: Eureka Server +This section describes how to set up a Eureka server. +
    +How to Include Eureka Server +To include Eureka Server in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-eureka-server. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. + +If your project already uses Thymeleaf as its template engine, the Freemarker templates of the Eureka server may not be loaded correctly. In this case it is necessary to configure the template loader manually: + + +application.yml + +spring: + freemarker: + template-loader-path: classpath:/templates/ + prefer-file-system-access: false + + +
    +
    +How to Run a Eureka Server +The following example shows a minimal Eureka server: +@SpringBootApplication +@EnableEurekaServer +public class Application { + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} +The server has a home page with a UI and HTTP API endpoints for the normal Eureka functionality under /eureka/*. +The following links have some Eureka background reading: flux capacitor and google group discussion. + +Due to Gradle’s dependency resolution rules and the lack of a parent bom feature, depending on spring-cloud-starter-netflix-eureka-server can cause failures on application startup. +To remedy this issue, add the Spring Boot Gradle plugin and import the Spring cloud starter parent bom as follows: + +build.gradle + +buildscript { + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:{spring-boot-docs-version}") + } +} + +apply plugin: "spring-boot" + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:{spring-cloud-version}" + } +} + + + +
    +
    +High Availability, Zones and Regions +The Eureka server does not have a back end store, but the service instances in the registry all have to send heartbeats to keep their registrations up to date (so this can be done in memory). +Clients also have an in-memory cache of Eureka registrations (so they do not have to go to the registry for every request to a service). +By default, every Eureka server is also a Eureka client and requires (at least one) service URL to locate a peer. +If you do not provide it, the service runs and works, but it fills your logs with a lot of noise about not being able to register with the peer. +See also below for details of Ribbon support on the client side for Zones and Regions. +
    +
    +Standalone Mode +The combination of the two caches (client and server) and the heartbeats make a standalone Eureka server fairly resilient to failure, as long as there is some sort of monitor or elastic runtime (such as Cloud Foundry) keeping it alive. +In standalone mode, you might prefer to switch off the client side behavior so that it does not keep trying and failing to reach its peers. +The following example shows how to switch off the client-side behavior: + +application.yml (Standalone Eureka Server) + +server: + port: 8761 + +eureka: + instance: + hostname: localhost + client: + registerWithEureka: false + fetchRegistry: false + serviceUrl: + defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ + + +Notice that the serviceUrl is pointing to the same host as the local instance. +
    +
    +Peer Awareness +Eureka can be made even more resilient and available by running multiple instances and asking them to register with each other. +In fact, this is the default behavior, so all you need to do to make it work is add a valid serviceUrl to a peer, as shown in the following example: + +application.yml (Two Peer Aware Eureka Servers) + +--- +spring: + profiles: peer1 +eureka: + instance: + hostname: peer1 + client: + serviceUrl: + defaultZone: http://peer2/eureka/ + +--- +spring: + profiles: peer2 +eureka: + instance: + hostname: peer2 + client: + serviceUrl: + defaultZone: http://peer1/eureka/ + + +In the preceding example, we have a YAML file that can be used to run the same server on two hosts (peer1 and peer2) by running it in different Spring profiles. +You could use this configuration to test the peer awareness on a single host (there is not much value in doing that in production) by manipulating /etc/hosts to resolve the host names. +In fact, the eureka.instance.hostname is not needed if you are running on a machine that knows its own hostname (by default, it is looked up by using java.net.InetAddress). +You can add multiple peers to a system, and, as long as they are all connected to each other by at least one edge, they synchronize +the registrations amongst themselves. +If the peers are physically separated (inside a data center or between multiple data centers), then the system can, in principle, survive split-brain type failures. +You can add multiple peers to a system, and as long as they are all +directly connected to each other, they will synchronize +the registrations amongst themselves. + +application.yml (Three Peer Aware Eureka Servers) + +eureka: + client: + serviceUrl: + defaultZone: http://peer1/eureka/,http://peer2/eureka/,http://peer3/eureka/ + +--- +spring: + profiles: peer1 +eureka: + instance: + hostname: peer1 + +--- +spring: + profiles: peer2 +eureka: + instance: + hostname: peer2 + +--- +spring: + profiles: peer3 +eureka: + instance: + hostname: peer3 + + +
    +
    +When to Prefer IP Address +In some cases, it is preferable for Eureka to advertise the IP addresses of services rather than the hostname. +Set eureka.instance.preferIpAddress to true and, when the application registers with eureka, it uses its IP address rather than its hostname. + +If the hostname cannot be determined by Java, then the IP address is sent to Eureka. +Only explict way of setting the hostname is by setting eureka.instance.hostname property. +You can set your hostname at the run-time by using an environment variable — for example, eureka.instance.hostname=${HOST_NAME}. + +
    +
    +Securing The Eureka Server +You can secure your Eureka server simply by adding Spring Security to your +server’s classpath via spring-boot-starter-security. By default when Spring Security is on the classpath it will require that +a valid CSRF token be sent with every request to the app. Eureka clients will not generally possess a valid +cross site request forgery (CSRF) token you will need to disable this requirement for the /eureka/** endpoints. +For example: +@EnableWebSecurity +class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().ignoringAntMatchers("/eureka/**"); + super.configure(http); + } +} +For more information on CSRF see the Spring Security documentation. +A demo Eureka Server can be found in the Spring Cloud Samples repo. +
    +
    +JDK 11 Support +The JAXB modules which the Eureka server depends upon were removed in JDK 11. If you intend to use JDK 11 +when running a Eureka server you must include these dependencies in your POM or Gradle file. +<dependency> + <groupId>org.glassfish.jaxb</groupId> + <artifactId>jaxb-runtime</artifactId> +</dependency> +
    +
    + +Circuit Breaker: Hystrix Clients +Netflix has created a library called Hystrix that implements the circuit breaker pattern. +In a microservice architecture, it is common to have multiple layers of service calls, as shown in the following example: +
    +Microservice Graph + + + + +Hystrix + +
    +A service failure in the lower level of services can cause cascading failure all the way up to the user. +When calls to a particular service exceed circuitBreaker.requestVolumeThreshold (default: 20 requests) and the failure percentage is greater than circuitBreaker.errorThresholdPercentage (default: >50%) in a rolling window defined by metrics.rollingStats.timeInMilliseconds (default: 10 seconds), the circuit opens and the call is not made. +In cases of error and an open circuit, a fallback can be provided by the developer. +
    +Hystrix fallback prevents cascading failures + + + + +HystrixFallback + +
    +Having an open circuit stops cascading failures and allows overwhelmed or failing services time to recover. +The fallback can be another Hystrix protected call, static data, or a sensible empty value. +Fallbacks may be chained so that the first fallback makes some other business call, which in turn falls back to static data. +
    +How to Include Hystrix +To include Hystrix in your project, use the starter with a group ID of org.springframework.cloud +and a artifact ID of spring-cloud-starter-netflix-hystrix. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +The following example shows a minimal Eureka server with a Hystrix circuit breaker: +@SpringBootApplication +@EnableCircuitBreaker +public class Application { + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} + +@Component +public class StoreIntegration { + + @HystrixCommand(fallbackMethod = "defaultStores") + public Object getStores(Map<String, Object> parameters) { + //do stuff that might fail + } + + public Object defaultStores(Map<String, Object> parameters) { + return /* something useful */; + } +} +The @HystrixCommand is provided by a Netflix contrib library called javanica. +Spring Cloud automatically wraps Spring beans with that annotation in a proxy that is connected to the Hystrix circuit breaker. +The circuit breaker calculates when to open and close the circuit and what to do in case of a failure. +To configure the @HystrixCommand you can use the commandProperties +attribute with a list of @HystrixProperty annotations. See +here +for more details. See the Hystrix wiki +for details on the properties available. +
    +
    +Propagating the Security Context or Using Spring Scopes +If you want some thread local context to propagate into a @HystrixCommand, the default declaration does not work, because it executes the command in a thread pool (in case of timeouts). +You can switch Hystrix to use the same thread as the caller through configuration or directly in the annotation, by asking it to use a different Isolation Strategy. +The following example demonstrates setting the thread in the annotation: +@HystrixCommand(fallbackMethod = "stubMyService", + commandProperties = { + @HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE") + } +) +... +The same thing applies if you are using @SessionScope or @RequestScope. +If you encounter a runtime exception that says it cannot find the scoped context, you need to use the same thread. +You also have the option to set the hystrix.shareSecurityContext property to true. +Doing so auto-configures a Hystrix concurrency strategy plugin hook to transfer the SecurityContext from your main thread to the one used by the Hystrix command. +Hystrix does not let multiple Hystrix concurrency strategy be registered so an extension mechanism is available by declaring your own HystrixConcurrencyStrategy as a Spring bean. +Spring Cloud looks for your implementation within the Spring context and wrap it inside its own plugin. +
    +
    +Health Indicator +The state of the connected circuit breakers are also exposed in the /health endpoint of the calling application, as shown in the following example: +{ + "hystrix": { + "openCircuitBreakers": [ + "StoreIntegration::getStoresByLocationLink" + ], + "status": "CIRCUIT_OPEN" + }, + "status": "UP" +} +
    +
    +Hystrix Metrics Stream +To enable the Hystrix metrics stream, include a dependency on spring-boot-starter-actuator and set +management.endpoints.web.exposure.include: hystrix.stream. +Doing so exposes the /actuator/hystrix.stream as a management endpoint, as shown in the following example: + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> + </dependency> +
    +
    + +Circuit Breaker: Hystrix Dashboard +One of the main benefits of Hystrix is the set of metrics it gathers about each HystrixCommand. +The Hystrix Dashboard displays the health of each circuit breaker in an efficient manner. +
    +Hystrix Dashboard + + + + +Hystrix + +
    +
    + +Hystrix Timeouts And Ribbon Clients +When using Hystrix commands that wrap Ribbon clients you want to make sure your Hystrix timeout +is configured to be longer than the configured Ribbon timeout, including any potential +retries that might be made. For example, if your Ribbon connection timeout is one second and +the Ribbon client might retry the request three times, than your Hystrix timeout should +be slightly more than three seconds. +
    +How to Include the Hystrix Dashboard +To include the Hystrix Dashboard in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-hystrix-dashboard. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +To run the Hystrix Dashboard, annotate your Spring Boot main class with @EnableHystrixDashboard. +Then visit /hystrix and point the dashboard to an individual instance’s /hystrix.stream endpoint in a Hystrix client application. + +When connecting to a /hystrix.stream endpoint that uses HTTPS, the certificate used by the server must be trusted by the JVM. +If the certificate is not trusted, you must import the certificate into the JVM in order for the Hystrix Dashboard to make a successful connection to the stream endpoint. + +
    +
    +Turbine +Looking at an individual instance’s Hystrix data is not very useful in terms of the overall health of the system. Turbine is an application that aggregates all of the relevant /hystrix.stream endpoints into a combined /turbine.stream for use in the Hystrix Dashboard. +Individual instances are located through Eureka. +Running Turbine requires annotating your main class with the @EnableTurbine annotation (for example, by using spring-cloud-starter-netflix-turbine to set up the classpath). +All of the documented configuration properties from the Turbine 1 wiki apply. +The only difference is that the turbine.instanceUrlSuffix does not need the port prepended, as this is handled automatically unless turbine.instanceInsertPort=false. + +By default, Turbine looks for the /hystrix.stream endpoint on a registered instance by looking up its hostName and port entries in Eureka and then appending /hystrix.stream to it. +If the instance’s metadata contains management.port, it is used instead of the port value for the /hystrix.stream endpoint. +By default, the metadata entry called management.port is equal to the management.port configuration property. +It can be overridden though with following configuration: + +eureka: + instance: + metadata-map: + management.port: ${management.port:8081} +The turbine.appConfig configuration key is a list of Eureka serviceIds that turbine uses to lookup instances. +The turbine stream is then used in the Hystrix dashboard with a URL similar to the following: +https://my.turbine.server:8080/turbine.stream?cluster=CLUSTERNAME +The cluster parameter can be omitted if the name is default. +The cluster parameter must match an entry in turbine.aggregator.clusterConfig. +Values returned from Eureka are upper-case. Consequently, the following example works if there is an application called customers registered with Eureka: +turbine: + aggregator: + clusterConfig: CUSTOMERS + appConfig: customers +If you need to customize which cluster names should be used by Turbine (because you do not want to store cluster names in +turbine.aggregator.clusterConfig configuration), provide a bean of type TurbineClustersProvider. +The clusterName can be customized by a SPEL expression in turbine.clusterNameExpression with root as an instance of InstanceInfo. +The default value is appName, which means that the Eureka serviceId becomes the cluster key (that is, the InstanceInfo for customers has an appName of CUSTOMERS). +A different example is turbine.clusterNameExpression=aSGName, which gets the cluster name from the AWS ASG name. +The following listing shows another example: +turbine: + aggregator: + clusterConfig: SYSTEM,USER + appConfig: customers,stores,ui,admin + clusterNameExpression: metadata['cluster'] +In the preceding example, the cluster name from four services is pulled from their metadata map and is expected to have values that include SYSTEM and USER. +To use the default cluster for all apps, you need a string literal expression (with single quotes and escaped with double quotes if it is in YAML as well): +turbine: + appConfig: customers,stores + clusterNameExpression: "'default'" +Spring Cloud provides a spring-cloud-starter-netflix-turbine that has all the dependencies you need to get a Turbine server running. To add Turbine, create a Spring Boot application and annotate it with @EnableTurbine. + +By default, Spring Cloud lets Turbine use the host and port to allow multiple processes per host, per cluster. +If you want the native Netflix behavior built into Turbine to not allow multiple processes per host, per cluster (the key to the instance ID is the hostname), set turbine.combineHostPort=false. + +
    +Clusters Endpoint +In some situations it might be useful for other applications to know what custers have been configured +in Turbine. To support this you can use the /clusters endpoint which will return a JSON array of +all the configured clusters. + +GET /clusters + +[ + { + "name": "RACES", + "link": "http://localhost:8383/turbine.stream?cluster=RACES" + }, + { + "name": "WEB", + "link": "http://localhost:8383/turbine.stream?cluster=WEB" + } +] + + +This endpoint can be disabled by setting turbine.endpoints.clusters.enabled to false. +
    +
    +
    +Turbine Stream +In some environments (such as in a PaaS setting), the classic Turbine model of pulling metrics from all the distributed Hystrix commands does not work. +In that case, you might want to have your Hystrix commands push metrics to Turbine. Spring Cloud enables that with messaging. +To do so on the client, add a dependency to spring-cloud-netflix-hystrix-stream and the spring-cloud-starter-stream-* of your choice. +See the Spring Cloud Stream documentation for details on the brokers and how to configure the client credentials. It should work out of the box for a local broker. +On the server side, create a Spring Boot application and annotate it with @EnableTurbineStream. +The Turbine Stream server requires the use of Spring Webflux, therefore spring-boot-starter-webflux needs to be included in your project. +By default spring-boot-starter-webflux is included when adding spring-cloud-starter-netflix-turbine-stream to your application. +You can then point the Hystrix Dashboard to the Turbine Stream Server instead of individual Hystrix streams. +If Turbine Stream is running on port 8989 on myhost, then put http://myhost:8989 in the stream input field in the Hystrix Dashboard. +Circuits are prefixed by their respective serviceId, followed by a dot (.), and then the circuit name. +Spring Cloud provides a spring-cloud-starter-netflix-turbine-stream that has all the dependencies you need to get a Turbine Stream server running. +You can then add the Stream binder of your choice — such as spring-cloud-starter-stream-rabbit. +Turbine Stream server also supports the cluster parameter. +Unlike Turbine server, Turbine Stream uses eureka serviceIds as cluster names and these are not configurable. +If Turbine Stream server is running on port 8989 on my.turbine.server and you have two eureka serviceIds customers and products in your environment, the following URLs will be available on your Turbine Stream server. default and empty cluster name will provide all metrics that Turbine Stream server receives. +https://my.turbine.sever:8989/turbine.stream?cluster=customers +https://my.turbine.sever:8989/turbine.stream?cluster=products +https://my.turbine.sever:8989/turbine.stream?cluster=default +https://my.turbine.sever:8989/turbine.stream +So, you can use eureka serviceIds as cluster names for your Turbine dashboard (or any compatible dashboard). +You don’t need to configure any properties like turbine.appConfig, turbine.clusterNameExpression and turbine.aggregator.clusterConfig for your Turbine Stream server. + +Turbine Stream server gathers all metrics from the configured input channel with Spring Cloud Stream. It means that it doesn’t gather Hystrix metrics actively from each instance. It just can provide metrics that were already gathered into the input channel by each instance. + +
    +
    + +Client Side Load Balancer: Ribbon +Ribbon is a client-side load balancer that gives you a lot of control over the behavior of HTTP and TCP clients. +Feign already uses Ribbon, so, if you use @FeignClient, this section also applies. +A central concept in Ribbon is that of the named client. +Each load balancer is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer (for example, by using the @FeignClient annotation). +On demand, Spring Cloud creates a new ensemble as an ApplicationContext for each named client by using +RibbonClientConfiguration. +This contains (amongst other things) an ILoadBalancer, a RestClient, and a ServerListFilter. +
    +How to Include Ribbon +To include Ribbon in your project, use the starter with a group ID of org.springframework.cloud and an artifact ID of spring-cloud-starter-netflix-ribbon. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +
    +
    +Customizing the Ribbon Client +You can configure some bits of a Ribbon client by using external properties in <client>.ribbon.*, which is similar to using the Netflix APIs natively, except that you can use Spring Boot configuration files. +The native options can be inspected as static fields in CommonClientConfigKey (part of ribbon-core). +Spring Cloud also lets you take full control of the client by declaring additional configuration (on top of the RibbonClientConfiguration) using @RibbonClient, as shown in the following example: +@Configuration +@RibbonClient(name = "custom", configuration = CustomConfiguration.class) +public class TestConfiguration { +} +In this case, the client is composed from the components already in RibbonClientConfiguration, together with any in CustomConfiguration (where the latter generally overrides the former). + +The CustomConfiguration clas must be a @Configuration class, but take care that it is not in a @ComponentScan for the main application context. +Otherwise, it is shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication), you need to take steps to avoid it being included (for instance, you can put it in a separate, non-overlapping package or specify the packages to scan explicitly in the @ComponentScan). + +The following table shows the beans that Spring Cloud Netflix provides by default for Ribbon: + + + + + + + + + + +Bean Type +Bean Name +Class Name + + + + +IClientConfig +ribbonClientConfig +DefaultClientConfigImpl + + +IRule +ribbonRule +ZoneAvoidanceRule + + +IPing +ribbonPing +DummyPing + + +ServerList<Server> +ribbonServerList +ConfigurationBasedServerList + + +ServerListFilter<Server> +ribbonServerListFilter +ZonePreferenceServerListFilter + + +ILoadBalancer +ribbonLoadBalancer +ZoneAwareLoadBalancer + + +ServerListUpdater +ribbonServerListUpdater +PollingServerListUpdater + + + + +Creating a bean of one of those type and placing it in a @RibbonClient configuration (such as FooConfiguration above) lets you override each one of the beans described, as shown in the following example: +@Configuration +protected static class FooConfiguration { + + @Bean + public ZonePreferenceServerListFilter serverListFilter() { + ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter(); + filter.setZone("myTestZone"); + return filter; + } + + @Bean + public IPing ribbonPing() { + return new PingUrl(); + } + +} +The include statement in the preceding example replaces NoOpPing with PingUrl and provides a custom serverListFilter. +
    +
    +Customizing the Default for All Ribbon Clients +A default configuration can be provided for all Ribbon Clients by using the @RibbonClients annotation and registering a default configuration, as shown in the following example: +@RibbonClients(defaultConfiguration = DefaultRibbonConfig.class) +public class RibbonClientDefaultConfigurationTestsConfig { + + public static class BazServiceList extends ConfigurationBasedServerList { + + public BazServiceList(IClientConfig config) { + super.initWithNiwsConfig(config); + } + + } + +} + +@Configuration +class DefaultRibbonConfig { + + @Bean + public IRule ribbonRule() { + return new BestAvailableRule(); + } + + @Bean + public IPing ribbonPing() { + return new PingUrl(); + } + + @Bean + public ServerList<Server> ribbonServerList(IClientConfig config) { + return new RibbonClientDefaultConfigurationTestsConfig.BazServiceList(config); + } + + @Bean + public ServerListSubsetFilter serverListFilter() { + ServerListSubsetFilter filter = new ServerListSubsetFilter(); + return filter; + } + +} +
    +
    +Customizing the Ribbon Client by Setting Properties +Starting with version 1.2.0, Spring Cloud Netflix now supports customizing Ribbon clients by setting properties to be compatible with the Ribbon documentation. +This lets you change behavior at start up time in different environments. +The following list shows the supported properties>: + + +<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer + + +<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule + + +<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing + + +<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList + + +<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter + + + +Classes defined in these properties have precedence over beans defined by using @RibbonClient(configuration=MyRibbonConfig.class) and the defaults provided by Spring Cloud Netflix. + +To set the IRule for a service name called users, you could set the following properties: + +application.yml + +users: + ribbon: + NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList + NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule + + +See the Ribbon documentation for implementations provided by Ribbon. +
    +
    +Using Ribbon with Eureka +When Eureka is used in conjunction with Ribbon (that is, both are on the classpath), the ribbonServerList is overridden with an extension of DiscoveryEnabledNIWSServerList, which populates the list of servers from Eureka. +It also replaces the IPing interface with NIWSDiscoveryPing, which delegates to Eureka to determine if a server is up. +The ServerList that is installed by default is a DomainExtractingServerList. Its purpose is to make metadata available to the load balancer without using AWS AMI metadata (which is what Netflix relies on). +By default, the server list is constructed with zone information, as provided in the instance metadata (so, on the remote clients, set eureka.instance.metadataMap.zone). +If that is missing and if the approximateZoneFromHostname flag is set, it can use the domain name from the server hostname as a proxy for the zone. +Once the zone information is available, it can be used in a ServerListFilter. +By default, it is used to locate a server in the same zone as the client, because the default is a ZonePreferenceServerListFilter. +By default, the zone of the client is determined in the same way as the remote instances (that is, through eureka.instance.metadataMap.zone). + +The orthodox archaius way to set the client zone is through a configuration property called "@zone". +If it is available, Spring Cloud uses that in preference to all other settings (note that the key must be quoted in YAML configuration). + + +If there is no other source of zone data, then a guess is made, based on the client configuration (as opposed to the instance configuration). +We take eureka.client.availabilityZones, which is a map from region name to a list of zones, and pull out the first zone for the instance’s own region (that is, the eureka.client.region, which defaults to "us-east-1", for compatibility with native Netflix). + +
    +
    +Example: How to Use Ribbon Without Eureka +Eureka is a convenient way to abstract the discovery of remote servers so that you do not have to hard code their URLs in clients. +However, if you prefer not to use Eureka, Ribbon and Feign also work. +Suppose you have declared a @RibbonClient for "stores", and Eureka is not in use (and not even on the classpath). +The Ribbon client defaults to a configured server list. +You can supply the configuration as follows: + +application.yml + +stores: + ribbon: + listOfServers: example.com,google.com + + +
    +
    +Example: Disable Eureka Use in Ribbon +Setting the ribbon.eureka.enabled property to false explicitly disables the use of Eureka in Ribbon, as shown in the following example: + +application.yml + +ribbon: + eureka: + enabled: false + + +
    +
    +Using the Ribbon API Directly +You can also use the LoadBalancerClient directly, as shown in the following example: +public class MyClass { + @Autowired + private LoadBalancerClient loadBalancer; + + public void doStuff() { + ServiceInstance instance = loadBalancer.choose("stores"); + URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort())); + // ... do something with the URI + } +} +
    +
    +Caching of Ribbon Configuration +Each Ribbon named client has a corresponding child application Context that Spring Cloud maintains. +This application context is lazily loaded on the first request to the named client. +This lazy loading behavior can be changed to instead eagerly load these child application contexts at startup, by specifying the names of the Ribbon clients, as shown in the following example: + +application.yml + +ribbon: + eager-load: + enabled: true + clients: client1, client2, client3 + + +
    +
    +How to Configure Hystrix Thread Pools +If you change zuul.ribbonIsolationStrategy to THREAD, the thread isolation strategy for Hystrix is used for all routes. +In that case, the HystrixThreadPoolKey is set to RibbonCommand as the default. +It means that HystrixCommands for all routes are executed in the same Hystrix thread pool. +This behavior can be changed with the following configuration: + +application.yml + +zuul: + threadPool: + useSeparateThreadPools: true + + +The preceding example results in HystrixCommands being executed in the Hystrix thread pool for each route. +In this case, the default HystrixThreadPoolKey is the same as the service ID for each route. +To add a prefix to HystrixThreadPoolKey, set zuul.threadPool.threadPoolKeyPrefix to the value that you want to add, as shown in the following example: + +application.yml + +zuul: + threadPool: + useSeparateThreadPools: true + threadPoolKeyPrefix: zuulgw + + +
    +
    +How to Provide a Key to Ribbon’s <literal>IRule</literal> +If you need to provide your own IRule implementation to handle a special routing requirement like a canary test, pass some information to the choose method of IRule. + +com.netflix.loadbalancer.IRule.java + +public interface IRule{ + public Server choose(Object key); + : + + +You can provide some information that is used by your IRule implementation to choose a target server, as shown in the following example: +RequestContext.getCurrentContext() + .set(FilterConstants.LOAD_BALANCER_KEY, "canary-test"); +If you put any object into the RequestContext with a key of FilterConstants.LOAD_BALANCER_KEY, it is passed to the choose method of the IRule implementation. +The code shown in the preceding example must be executed before RibbonRoutingFilter is executed. +Zuul’s pre filter is the best place to do that. +You can access HTTP headers and query parameters through the RequestContext in pre filter, so it can be used to determine the LOAD_BALANCER_KEY that is passed to Ribbon. +If you do not put any value with LOAD_BALANCER_KEY in RequestContext, null is passed as a parameter of the choose method. +
    +
    + +External Configuration: Archaius +Archaius is the Netflix client-side configuration library. +It is the library used by all of the Netflix OSS components for configuration. +Archaius is an extension of the Apache Commons Configuration project. +It allows updates to configuration by either polling a source for changes or by letting a source push changes to the client. +Archaius uses Dynamic<Type>Property classes as handles to properties, as shown in the following example: + +Archaius Example + +class ArchaiusTest { + DynamicStringProperty myprop = DynamicPropertyFactory + .getInstance() + .getStringProperty("my.prop"); + + void doSomething() { + OtherClass.someMethod(myprop.get()); + } +} + + +Archaius has its own set of configuration files and loading priorities. +Spring applications should generally not use Archaius directly, but the need to configure the Netflix tools natively remains. +Spring Cloud has a Spring Environment Bridge so that Archaius can read properties from the Spring Environment. +This bridge allows Spring Boot projects to use the normal configuration toolchain while letting them configure the Netflix tools as documented (for the most part). + + +Router and Filter: Zuul +Routing is an integral part of a microservice architecture. +For example, / may be mapped to your web application, /api/users is mapped to the user service and /api/shop is mapped to the shop service. +Zuul is a JVM-based router and server-side load balancer from Netflix. +Netflix uses Zuul for the following: + + +Authentication + + +Insights + + +Stress Testing + + +Canary Testing + + +Dynamic Routing + + +Service Migration + + +Load Shedding + + +Security + + +Static Response handling + + +Active/Active traffic management + + +Zuul’s rule engine lets rules and filters be written in essentially any JVM language, with built-in support for Java and Groovy. + +The configuration property zuul.max.host.connections has been replaced by two new properties, zuul.host.maxTotalConnections and zuul.host.maxPerRouteConnections, which default to 200 and 20 respectively. + + +The default Hystrix isolation pattern (ExecutionIsolationStrategy) for all routes is SEMAPHORE. +zuul.ribbonIsolationStrategy can be changed to THREAD if that isolation pattern is preferred. + +
    +How to Include Zuul +To include Zuul in your project, use the starter with a group ID of org.springframework.cloud and a artifact ID of spring-cloud-starter-netflix-zuul. +See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +
    +
    +Embedded Zuul Reverse Proxy +Spring Cloud has created an embedded Zuul proxy to ease the development of a common use case where a UI application wants to make proxy calls to one or more back end services. +This feature is useful for a user interface to proxy to the back end services it requires, avoiding the need to manage CORS and authentication concerns independently for all the back ends. +To enable it, annotate a Spring Boot main class with @EnableZuulProxy. Doing so causes local calls to be forwarded to the appropriate service. +By convention, a service with an ID of users receives requests from the proxy located at /users (with the prefix stripped). +The proxy uses Ribbon to locate an instance to which to forward through discovery. +All requests are executed in a hystrix command, so failures appear in Hystrix metrics. +Once the circuit is open, the proxy does not try to contact the service. + +the Zuul starter does not include a discovery client, so, for routes based on service IDs, you need to provide one of those on the classpath as well (Eureka is one choice). + +To skip having a service automatically added, set zuul.ignored-services to a list of service ID patterns. +If a service matches a pattern that is ignored but is also included in the explicitly configured routes map, it is unignored, as shown in the following example: + +application.yml + + zuul: + ignoredServices: '*' + routes: + users: /myusers/** + + +In the preceding example, all services are ignored, except for users. +To augment or change the proxy routes, you can add external configuration, as follows: + +application.yml + + zuul: + routes: + users: /myusers/** + + +The preceding example means that HTTP calls to /myusers get forwarded to the users service (for example /myusers/101 is forwarded to /101). +To get more fine-grained control over a route, you can specify the path and the serviceId independently, as follows: + +application.yml + + zuul: + routes: + users: + path: /myusers/** + serviceId: users_service + + +The preceding example means that HTTP calls to /myusers get forwarded to the users_service service. +The route must have a path that can be specified as an ant-style pattern, so /myusers/* only matches one level, but /myusers/** matches hierarchically. +The location of the back end can be specified as either a serviceId (for a service from discovery) or a url (for a physical location), as shown in the following example: + +application.yml + + zuul: + routes: + users: + path: /myusers/** + url: https://example.com/users_service + + +These simple url-routes do not get executed as a HystrixCommand, nor do they load-balance multiple URLs with Ribbon. +To achieve those goals, you can specify a serviceId with a static list of servers, as follows: + +application.yml + +zuul: + routes: + echo: + path: /myusers/** + serviceId: myusers-service + stripPrefix: true + +hystrix: + command: + myusers-service: + execution: + isolation: + thread: + timeoutInMilliseconds: ... + +myusers-service: + ribbon: + NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList + listOfServers: https://example1.com,http://example2.com + ConnectTimeout: 1000 + ReadTimeout: 3000 + MaxTotalHttpConnections: 500 + MaxConnectionsPerHost: 100 + + +Another method is specifiying a service-route and configuring a Ribbon client for the serviceId (doing so requires disabling Eureka support in Ribbon — see above for more information), as shown in the following example: + +application.yml + +zuul: + routes: + users: + path: /myusers/** + serviceId: users + +ribbon: + eureka: + enabled: false + +users: + ribbon: + listOfServers: example.com,google.com + + +You can provide a convention between serviceId and routes by using regexmapper. +It uses regular-expression named groups to extract variables from serviceId and inject them into a route pattern, as shown in the following example: + +ApplicationConfiguration.java + +@Bean +public PatternServiceRouteMapper serviceRouteMapper() { + return new PatternServiceRouteMapper( + "(?<name>^.+)-(?<version>v.+$)", + "${version}/${name}"); +} + + +The preceding example means that a serviceId of myusers-v1 is mapped to route /v1/myusers/**. +Any regular expression is accepted, but all named groups must be present in both servicePattern and routePattern. +If servicePattern does not match a serviceId, the default behavior is used. +In the preceding example, a serviceId of myusers is mapped to the "/myusers/**" route (with no version detected). +This feature is disabled by default and only applies to discovered services. +To add a prefix to all mappings, set zuul.prefix to a value, such as /api. +By default, the proxy prefix is stripped from the request before the request is forwarded by (you can switch this behavior off with zuul.stripPrefix=false). +You can also switch off the stripping of the service-specific prefix from individual routes, as shown in the following example: + +application.yml + + zuul: + routes: + users: + path: /myusers/** + stripPrefix: false + + + +zuul.stripPrefix only applies to the prefix set in zuul.prefix. +It does not have any effect on prefixes defined within a given route’s path. + +In the preceding example, requests to /myusers/101 are forwarded to /myusers/101 on the users service. +The zuul.routes entries actually bind to an object of type ZuulProperties. +If you look at the properties of that object, you can see that it also has a retryable flag. +Set that flag to true to have the Ribbon client automatically retry failed requests. +You can also set that flag to true when you need to modify the parameters of the retry operations that use the Ribbon client configuration. +By default, the X-Forwarded-Host header is added to the forwarded requests. +To turn it off, set zuul.addProxyHeaders = false. +By default, the prefix path is stripped, and the request to the back end picks up a X-Forwarded-Prefix header (/myusers in the examples shown earlier). +If you set a default route (/), an application with @EnableZuulProxy could act as a standalone server. For example, zuul.route.home: / would route all traffic ("/**") to the "home" service. +If more fine-grained ignoring is needed, you can specify specific patterns to ignore. +These patterns are evaluated at the start of the route location process, which means prefixes should be included in the pattern to warrant a match. +Ignored patterns span all services and supersede any other route specification. +The following example shows how to create ignored patterns: + +application.yml + + zuul: + ignoredPatterns: /**/admin/** + routes: + users: /myusers/** + + +The preceding example means that all calls (such as /myusers/101) are forwarded to /101 on the users service. +However, calls including /admin/ do not resolve. + +If you need your routes to have their order preserved, you need to use a YAML file, as the ordering is lost when using a properties file. +The following example shows such a YAML file: + + +application.yml + + zuul: + routes: + users: + path: /myusers/** + legacy: + path: /** + + +If you were to use a properties file, the legacy path might end up in front of the users +path, rendering the users path unreachable. +
    +
    +Zuul Http Client +The default HTTP client used by Zuul is now backed by the Apache HTTP Client instead of the deprecated Ribbon RestClient. +To use RestClient or okhttp3.OkHttpClient, set ribbon.restclient.enabled=true or ribbon.okhttp.enabled=true, respectively. +If you would like to customize the Apache HTTP client or the OK HTTP client, provide a bean of type ClosableHttpClient or OkHttpClient. +
    +
    +Cookies and Sensitive Headers +You can share headers between services in the same system, but you probably do not want sensitive headers leaking downstream into external servers. +You can specify a list of ignored headers as part of the route configuration. +Cookies play a special role, because they have well defined semantics in browsers, and they are always to be treated as sensitive. +If the consumer of your proxy is a browser, then cookies for downstream services also cause problems for the user, because they all get jumbled up together (all downstream services look like they come from the same place). +If you are careful with the design of your services, (for example, if only one of the downstream services sets cookies), you might be able to let them flow from the back end all the way up to the caller. +Also, if your proxy sets cookies and all your back-end services are part of the same system, it can be natural to simply share them (and, for instance, use Spring Session to link them up to some shared state). +Other than that, any cookies that get set by downstream services are likely to be not useful to the caller, so it is recommended that you make (at least) Set-Cookie and Cookie into sensitive headers for routes that are not part of your domain. +Even for routes that are part of your domain, try to think carefully about what it means before letting cookies flow between them and the proxy. +The sensitive headers can be configured as a comma-separated list per route, as shown in the following example: + +application.yml + + zuul: + routes: + users: + path: /myusers/** + sensitiveHeaders: Cookie,Set-Cookie,Authorization + url: https://downstream + + + +This is the default value for sensitiveHeaders, so you need not set it unless you want it to be different. +This is new in Spring Cloud Netflix 1.1 (in 1.0, the user had no control over headers, and all cookies flowed in both directions). + +The sensitiveHeaders are a blacklist, and the default is not empty. +Consequently, to make Zuul send all headers (except the ignored ones), you must explicitly set it to the empty list. +Doing so is necessary if you want to pass cookie or authorization headers to your back end. The following example shows how to use sensitiveHeaders: + +application.yml + + zuul: + routes: + users: + path: /myusers/** + sensitiveHeaders: + url: https://downstream + + +You can also set sensitive headers, by setting zuul.sensitiveHeaders. +If sensitiveHeaders is set on a route, it overrides the global sensitiveHeaders setting. +
    +
    +Ignored Headers +In addition to the route-sensitive headers, you can set a global value called zuul.ignoredHeaders for values (both request and response) that should be discarded during interactions with downstream services. +By default, if Spring Security is not on the classpath, these are empty. +Otherwise, they are initialized to a set of well known security headers (for example, involving caching) as specified by Spring Security. +The assumption in this case is that the downstream services might add these headers, too, but we want the values from the proxy. +To not discard these well known security headers when Spring Security is on the classpath, you can set zuul.ignoreSecurityHeaders to false. +Doing so can be useful if you disabled the HTTP Security response headers in Spring Security and want the values provided by downstream services. +
    +
    +Management Endpoints +By default, if you use @EnableZuulProxy with the Spring Boot Actuator, you enable two additional endpoints: + + +Routes + + +Filters + + +
    +Routes Endpoint +A GET to the routes endpoint at /routes returns a list of the mapped routes: + +GET /routes + +{ + /stores/**: "http://localhost:8081" +} + + +Additional route details can be requested by adding the ?format=details query string to /routes. +Doing so produces the following output: + +GET /routes/details + +{ + "/stores/**": { + "id": "stores", + "fullPath": "/stores/**", + "location": "http://localhost:8081", + "path": "/**", + "prefix": "/stores", + "retryable": false, + "customSensitiveHeaders": false, + "prefixStripped": true + } +} + + +A POST to /routes forces a refresh of the existing routes (for example, when there have been changes in the service catalog). +You can disable this endpoint by setting endpoints.routes.enabled to false. + +the routes should respond automatically to changes in the service catalog, but the POST to /routes is a way to force the change +to happen immediately. + +
    +
    +Filters Endpoint +A GET to the filters endpoint at /filters returns a map of Zuul filters by type. +For each filter type in the map, you get a list of all the filters of that type, along with their details. +
    +
    +
    +Strangulation Patterns and Local Forwards +A common pattern when migrating an existing application or API is to strangle old endpoints, slowly replacing them with different implementations. +The Zuul proxy is a useful tool for this because you can use it to handle all traffic from the clients of the old endpoints but redirect some of the requests to new ones. +The following example shows the configuration details for a strangle scenario: + +application.yml + + zuul: + routes: + first: + path: /first/** + url: https://first.example.com + second: + path: /second/** + url: forward:/second + third: + path: /third/** + url: forward:/3rd + legacy: + path: /** + url: https://legacy.example.com + + +In the preceding example, we are strangle the legacy application, which is mapped to all requests that do not match one of the other patterns. +Paths in /first/** have been extracted into a new service with an external URL. +Paths in /second/** are forwarded so that they can be handled locally (for example, with a normal Spring @RequestMapping). +Paths in /third/** are also forwarded but with a different prefix (/third/foo is forwarded to /3rd/foo). + +The ignored patterns aren’t completely ignored, they just are not handled by the proxy (so they are also effectively forwarded locally). + +
    +
    +Uploading Files through Zuul +If you use @EnableZuulProxy, you can use the proxy paths to upload files and it should work, so long as the files are small. +For large files there is an alternative path that bypasses the Spring DispatcherServlet (to avoid multipart processing) in "/zuul/*". +In other words, if you have zuul.routes.customers=/customers/**, then you can POST large files to /zuul/customers/*. +The servlet path is externalized via zuul.servletPath. +If the proxy route takes you through a Ribbon load balancer, extremely large files also require elevated timeout settings, as shown in the following example: + +application.yml + +hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000 +ribbon: + ConnectTimeout: 3000 + ReadTimeout: 60000 + + +Note that, for streaming to work with large files, you need to use chunked encoding in the request (which some browsers do not do by default), as shown in the following example: +$ curl -v -H "Transfer-Encoding: chunked" \ + -F "file=@mylarge.iso" localhost:9999/zuul/simple/file +
    +
    +Query String Encoding +When processing the incoming request, query params are decoded so that they can be available for possible modifications in Zuul filters. +They are then re-encoded the back end request is rebuilt in the route filters. +The result can be different than the original input if (for example) it was encoded with Javascript’s encodeURIComponent() method. +While this causes no issues in most cases, some web servers can be picky with the encoding of complex query string. +To force the original encoding of the query string, it is possible to pass a special flag to ZuulProperties so that the query string is taken as is with the HttpServletRequest::getQueryString method, as shown in the following example: + +application.yml + + zuul: + forceOriginalQueryStringEncoding: true + + + +This special flag works only with SimpleHostRoutingFilter. Also, you loose the ability to easily override +query parameters with RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters), because +the query string is now fetched directly on the original HttpServletRequest. + +
    +
    +Request URI Encoding +When processing the incoming request, request URI is decoded before matching them to routes. +The request URI is then re-encoded when the back end request is rebuilt in the route filters. +This can cause some unexpected behavior if your URI includes the encoded "/" character. +To use the original request URI, it is possible to pass a special flag to 'ZuulProperties' so that the URI will be taken as is with the HttpServletRequest::getRequestURI method, as shown in the following example: + +application.yml + + zuul: + decodeUrl: false + + + +If you are overriding request URI using requestURI RequestContext attribute and this flag is set to false, then the URL set in the request context will not be encoded. It will be your responsibility to make sure the URL is already encoded. + +
    +
    +Plain Embedded Zuul +If you use @EnableZuulServer (instead of @EnableZuulProxy), you can also run a Zuul server without proxying or selectively switch on parts of the proxying platform. +Any beans that you add to the application of type ZuulFilter are installed automatically (as they are with @EnableZuulProxy) but without any of the proxy filters being added automatically. +In that case, the routes into the Zuul server are still specified by configuring "zuul.routes.*", but there is no service discovery and no proxying. Consequently, the "serviceId" and "url" settings are ignored. +The following example maps all paths in "/api/**" to the Zuul filter chain: + +application.yml + + zuul: + routes: + api: /api/** + + +
    +
    +Disable Zuul Filters +Zuul for Spring Cloud comes with a number of ZuulFilter beans enabled by default in both proxy and server mode. +See the Zuul filters package for the list of filters that you can enable. +If you want to disable one, set zuul.<SimpleClassName>.<filterType>.disable=true. +By convention, the package after filters is the Zuul filter type. +For example to disable org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter, set zuul.SendResponseFilter.post.disable=true. +
    +
    +Providing Hystrix Fallbacks For Routes +When a circuit for a given route in Zuul is tripped, you can provide a fallback response by creating a bean of type FallbackProvider. +Within this bean, you need to specify the route ID the fallback is for and provide a ClientHttpResponse to return as a fallback. +The following example shows a relatively simple FallbackProvider implementation: +class MyFallbackProvider implements FallbackProvider { + + @Override + public String getRoute() { + return "customers"; + } + + @Override + public ClientHttpResponse fallbackResponse(String route, final Throwable cause) { + if (cause instanceof HystrixTimeoutException) { + return response(HttpStatus.GATEWAY_TIMEOUT); + } else { + return response(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + private ClientHttpResponse response(final HttpStatus status) { + return new ClientHttpResponse() { + @Override + public HttpStatus getStatusCode() throws IOException { + return status; + } + + @Override + public int getRawStatusCode() throws IOException { + return status.value(); + } + + @Override + public String getStatusText() throws IOException { + return status.getReasonPhrase(); + } + + @Override + public void close() { + } + + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream("fallback".getBytes()); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } + }; + } +} +The following example shows how the route configuration for the previous example might appear: +zuul: + routes: + customers: /customers/** +If you would like to provide a default fallback for all routes, you can create a bean of type FallbackProvider and have the getRoute method return * or null, as shown in the following example: +class MyFallbackProvider implements FallbackProvider { + @Override + public String getRoute() { + return "*"; + } + + @Override + public ClientHttpResponse fallbackResponse(String route, Throwable throwable) { + return new ClientHttpResponse() { + @Override + public HttpStatus getStatusCode() throws IOException { + return HttpStatus.OK; + } + + @Override + public int getRawStatusCode() throws IOException { + return 200; + } + + @Override + public String getStatusText() throws IOException { + return "OK"; + } + + @Override + public void close() { + + } + + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream("fallback".getBytes()); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return headers; + } + }; + } +} +
    +
    +Zuul Timeouts +If you want to configure the socket timeouts and read timeouts for requests proxied through Zuul, you have two options, based on your configuration: + + +If Zuul uses service discovery, you need to configure these timeouts with the +ribbon.ReadTimeout and ribbon.SocketTimeout Ribbon properties. + + +If you have configured Zuul routes by specifying URLs, you need to use +zuul.host.connect-timeout-millis and zuul.host.socket-timeout-millis. +
    +
    +Rewriting the <literal>Location</literal> header +If Zuul is fronting a web application, you may need to re-write the Location header when the web application redirects through a HTTP status code of 3XX. +Otherwise, the browser redirects to the web application’s URL instead of the Zuul URL. +You can configure a LocationRewriteFilter Zuul filter to re-write the Location header to the Zuul’s URL. +It also adds back the stripped global and route-specific prefixes. +The following example adds a filter by using a Spring Configuration file: +import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter; +... + +@Configuration +@EnableZuulProxy +public class ZuulConfig { + @Bean + public LocationRewriteFilter locationRewriteFilter() { + return new LocationRewriteFilter(); + } +} + +Use this filter carefully. The filter acts on the Location header of ALL 3XX response codes, which may not be appropriate in all scenarios, such as when redirecting the user to an external URL. + +
    +
    +Enabling Cross Origin Requests +By default Zuul routes all Cross Origin requests (CORS) to the services. If you want instead Zuul to handle these requests it can be done by providing custom WebMvcConfigurer bean: +@Bean +public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/path-1/**") + .allowedOrigins("https://allowed-origin.com") + .allowedMethods("GET", "POST"); + } + }; +} +In the example above, we allow GET and POST methods from https://allowed-origin.com to send cross-origin requests to the endpoints starting with path-1. +You can apply CORS configuration to a specific path pattern or globally for the whole application, using /** mapping. +You can customize properties: allowedOrigins,allowedMethods,allowedHeaders,exposedHeaders,allowCredentials and maxAge via this configuration. +
    +
    +Metrics +Zuul will provide metrics under the Actuator metrics endpoint for any failures that might occur when routing requests. +These metrics can be viewed by hitting /actuator/metrics. The metrics will have a name that has the format +ZUUL::EXCEPTION:errorCause:statusCode. +
    +
    +Zuul Developer Guide +For a general overview of how Zuul works, see the Zuul Wiki. +
    +The Zuul Servlet +Zuul is implemented as a Servlet. For the general cases, Zuul is embedded into the Spring Dispatch mechanism. This lets Spring MVC be in control of the routing. +In this case, Zuul buffers requests. +If there is a need to go through Zuul without buffering requests (for example, for large file uploads), the Servlet is also installed outside of the Spring Dispatcher. +By default, the servlet has an address of /zuul. +This path can be changed with the zuul.servlet-path property. +
    +
    +Zuul RequestContext +To pass information between filters, Zuul uses a RequestContext. +Its data is held in a ThreadLocal specific to each request. +Information about where to route requests, errors, and the actual HttpServletRequest and HttpServletResponse are stored there. +The RequestContext extends ConcurrentHashMap, so anything can be stored in the context. FilterConstants contains the keys used by the filters installed by Spring Cloud Netflix (more on these later). +
    +
    +<literal>@EnableZuulProxy</literal> vs. <literal>@EnableZuulServer</literal> +Spring Cloud Netflix installs a number of filters, depending on which annotation was used to enable Zuul. @EnableZuulProxy is a superset of @EnableZuulServer. In other words, @EnableZuulProxy contains all the filters installed by @EnableZuulServer. The additional filters in the proxy enable routing functionality. If you want a blank Zuul, you should use @EnableZuulServer. +
    +
    +<literal>@EnableZuulServer</literal> Filters +@EnableZuulServer creates a SimpleRouteLocator that loads route definitions from Spring Boot configuration files. +The following filters are installed (as normal Spring Beans): + + +Pre filters: + + +ServletDetectionFilter: Detects whether the request is through the Spring Dispatcher. Sets a boolean with a key of FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY. + + +FormBodyWrapperFilter: Parses form data and re-encodes it for downstream requests. + + +DebugFilter: If the debug request parameter is set, sets RequestContext.setDebugRouting() and RequestContext.setDebugRequest() to true. +*Route filters: + + +SendForwardFilter: Forwards requests by using the Servlet RequestDispatcher. The forwarding location is stored in the RequestContext attribute, FilterConstants.FORWARD_TO_KEY. This is useful for forwarding to endpoints in the current application. + + + + +Post filters: + + +SendResponseFilter: Writes responses from proxied requests to the current response. + + + + +Error filters: + + +SendErrorFilter: Forwards to /error (by default) if RequestContext.getThrowable() is not null. You can change the default forwarding path (/error) by setting the error.path property. + + + + +
    +
    +<literal>@EnableZuulProxy</literal> Filters +Creates a DiscoveryClientRouteLocator that loads route definitions from a DiscoveryClient (such as Eureka) as well as from properties. A route is created for each serviceId from the DiscoveryClient. As new services are added, the routes are refreshed. +In addition to the filters described earlier, the following filters are installed (as normal Spring Beans): + + +Pre filters: + + +PreDecorationFilter: Determines where and how to route, depending on the supplied RouteLocator. It also sets various proxy-related headers for downstream requests. + + + + +Route filters: + + +RibbonRoutingFilter: Uses Ribbon, Hystrix, and pluggable HTTP clients to send requests. Service IDs are found in the RequestContext attribute, FilterConstants.SERVICE_ID_KEY. This filter can use different HTTP clients: + + +Apache HttpClient: The default client. + + +Squareup OkHttpClient v3: Enabled by having the com.squareup.okhttp3:okhttp library on the classpath and setting ribbon.okhttp.enabled=true. + + +Netflix Ribbon HTTP client: Enabled by setting ribbon.restclient.enabled=true. This client has limitations, including that it does not support the PATCH method, but it also has built-in retry. + + + + +SimpleHostRoutingFilter: Sends requests to predetermined URLs through an Apache HttpClient. URLs are found in RequestContext.getRouteHost(). + + + + +
    +
    +Custom Zuul Filter Examples +Most of the following "How to Write" examples below are included Sample Zuul Filters project. There are also examples of manipulating the request or response body in that repository. +This section includes the following examples: + + + + + + + + + + + +
    +How to Write a Pre Filter +Pre filters set up data in the RequestContext for use in filters downstream. +The main use case is to set information required for route filters. +The following example shows a Zuul pre filter: +public class QueryParamPreFilter extends ZuulFilter { + @Override + public int filterOrder() { + return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration + } + + @Override + public String filterType() { + return PRE_TYPE; + } + + @Override + public boolean shouldFilter() { + RequestContext ctx = RequestContext.getCurrentContext(); + return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded + && !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId + } + @Override + public Object run() { + RequestContext ctx = RequestContext.getCurrentContext(); + HttpServletRequest request = ctx.getRequest(); + if (request.getParameter("sample") != null) { + // put the serviceId in `RequestContext` + ctx.put(SERVICE_ID_KEY, request.getParameter("foo")); + } + return null; + } +} +The preceding filter populates SERVICE_ID_KEY from the sample request parameter. +In practice, you should not do that kind of direct mapping. Instead, the service ID should be looked up from the value of sample instead. +Now that SERVICE_ID_KEY is populated, PreDecorationFilter does not run and RibbonRoutingFilter runs. + +If you want to route to a full URL, call ctx.setRouteHost(url) instead. + +To modify the path to which routing filters forward, set the REQUEST_URI_KEY. +
    +
    +How to Write a Route Filter +Route filters run after pre filters and make requests to other services. +Much of the work here is to translate request and response data to and from the model required by the client. +The following example shows a Zuul route filter: +public class OkHttpRoutingFilter extends ZuulFilter { + @Autowired + private ProxyRequestHelper helper; + + @Override + public String filterType() { + return ROUTE_TYPE; + } + + @Override + public int filterOrder() { + return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1; + } + + @Override + public boolean shouldFilter() { + return RequestContext.getCurrentContext().getRouteHost() != null + && RequestContext.getCurrentContext().sendZuulResponse(); + } + + @Override + public Object run() { + OkHttpClient httpClient = new OkHttpClient.Builder() + // customize + .build(); + + RequestContext context = RequestContext.getCurrentContext(); + HttpServletRequest request = context.getRequest(); + + String method = request.getMethod(); + + String uri = this.helper.buildZuulRequestURI(request); + + Headers.Builder headers = new Headers.Builder(); + Enumeration<String> headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + Enumeration<String> values = request.getHeaders(name); + + while (values.hasMoreElements()) { + String value = values.nextElement(); + headers.add(name, value); + } + } + + InputStream inputStream = request.getInputStream(); + + RequestBody requestBody = null; + if (inputStream != null && HttpMethod.permitsRequestBody(method)) { + MediaType mediaType = null; + if (headers.get("Content-Type") != null) { + mediaType = MediaType.parse(headers.get("Content-Type")); + } + requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream)); + } + + Request.Builder builder = new Request.Builder() + .headers(headers.build()) + .url(uri) + .method(method, requestBody); + + Response response = httpClient.newCall(builder.build()).execute(); + + LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>(); + + for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) { + responseHeaders.put(entry.getKey(), entry.getValue()); + } + + this.helper.setResponse(response.code(), response.body().byteStream(), + responseHeaders); + context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running + return null; + } +} +The preceding filter translates Servlet request information into OkHttp3 request information, executes an HTTP request, and translates OkHttp3 response information to the Servlet response. +
    +
    +How to Write a Post Filter +Post filters typically manipulate the response. The following filter adds a random UUID as the X-Sample header: +public class AddResponseHeaderFilter extends ZuulFilter { + @Override + public String filterType() { + return POST_TYPE; + } + + @Override + public int filterOrder() { + return SEND_RESPONSE_FILTER_ORDER - 1; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + RequestContext context = RequestContext.getCurrentContext(); + HttpServletResponse servletResponse = context.getResponse(); + servletResponse.addHeader("X-Sample", UUID.randomUUID().toString()); + return null; + } +} + +Other manipulations, such as transforming the response body, are much more complex and computationally intensive. + +
    +
    +
    +How Zuul Errors Work +If an exception is thrown during any portion of the Zuul filter lifecycle, the error filters are executed. +The SendErrorFilter is only run if RequestContext.getThrowable() is not null. +It then sets specific javax.servlet.error.* attributes in the request and forwards the request to the Spring Boot error page. +
    +
    +Zuul Eager Application Context Loading +Zuul internally uses Ribbon for calling the remote URLs. +By default, Ribbon clients are lazily loaded by Spring Cloud on first call. +This behavior can be changed for Zuul by using the following configuration, which results eager loading of the child Ribbon related Application contexts at application startup time. +The following example shows how to enable eager loading: + +application.yml + +zuul: + ribbon: + eager-load: + enabled: true + + +
    +
    +
    + +Polyglot support with Sidecar +Do you have non-JVM languages with which you want to take advantage of Eureka, Ribbon, and Config Server? +The Spring Cloud Netflix Sidecar was inspired by Netflix Prana. +It includes an HTTP API to get all of the instances (by host and port) for a given service. +You can also proxy service calls through an embedded Zuul proxy that gets its route entries from Eureka. +The Spring Cloud Config Server can be accessed directly through host lookup or through the Zuul Proxy. +The non-JVM application should implement a health check so the Sidecar can report to Eureka whether the app is up or down. +To include Sidecar in your project, use the dependency with a group ID of org.springframework.cloud +and artifact ID or spring-cloud-netflix-sidecar. +To enable the Sidecar, create a Spring Boot application with @EnableSidecar. +This annotation includes @EnableCircuitBreaker, @EnableDiscoveryClient, and @EnableZuulProxy. +Run the resulting application on the same host as the non-JVM application. +To configure the side car, add sidecar.port and sidecar.health-uri to application.yml. +The sidecar.port property is the port on which the non-JVM application listens. +This is so the Sidecar can properly register the application with Eureka. +The sidecar.secure-port-enabled options provides a way to enable secure port for traffic. +The sidecar.health-uri is a URI accessible on the non-JVM application that mimics a Spring Boot health indicator. +It should return a JSON document that resembles the following: + +health-uri-document + +{ + "status":"UP" +} + + +The following application.yml example shows sample configuration for a Sidecar application: + +application.yml + +server: + port: 5678 +spring: + application: + name: sidecar + +sidecar: + port: 8000 + health-uri: http://localhost:8000/health.json + + +The API for the DiscoveryClient.getInstances() method is /hosts/{serviceId}. +The following example response for /hosts/customers returns two instances on different hosts: + +/hosts/customers + +[ + { + "host": "myhost", + "port": 9000, + "uri": "http://myhost:9000", + "serviceId": "CUSTOMERS", + "secure": false + }, + { + "host": "myhost2", + "port": 9000, + "uri": "http://myhost2:9000", + "serviceId": "CUSTOMERS", + "secure": false + } +] + + +This API is accessible to the non-JVM application (if the sidecar is on port 5678) at http://localhost:5678/hosts/{serviceId}. +The Zuul proxy automatically adds routes for each service known in Eureka to /<serviceId>, so the customers service is available at /customers. +The non-JVM application can access the customer service at http://localhost:5678/customers (assuming the sidecar is listening on port 5678). +If the Config Server is registered with Eureka, the non-JVM application can access it through the Zuul proxy. +If the serviceId of the ConfigServer is configserver and the Sidecar is on port 5678, then it can be accessed at http://localhost:5678/configserver. +Non-JVM applications can take advantage of the Config Server’s ability to return YAML documents. +For example, a call to https://sidecar.local.spring.io:5678/configserver/default-master.yml +might result in a YAML document resembling the following: +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + password: password +info: + description: Spring Cloud Samples + url: https://github.com/spring-cloud-samples +To enable the health check request to accept all certificates when using HTTPs set sidecar.accept-all-ssl-certificates to `true. + + +Retrying Failed Requests +Spring Cloud Netflix offers a variety of ways to make HTTP requests. +You can use a load balanced RestTemplate, Ribbon, or Feign. +No matter how you choose to create your HTTP requests, there is always a chance that a request may fail. +When a request fails, you may want to have the request be retried automatically. +To do so when using Sping Cloud Netflix, you need to include Spring Retry on your application’s classpath. +When Spring Retry is present, load-balanced RestTemplates, Feign, and Zuul automatically retry any failed requests (assuming your configuration allows doing so). +
    +BackOff Policies +By default, no backoff policy is used when retrying requests. +If you would like to configure a backoff policy, you need to create a bean of type LoadBalancedRetryFactory and override the createBackOffPolicy method for a given service, as shown in the following example: +@Configuration +public class MyConfiguration { + @Bean + LoadBalancedRetryFactory retryFactory() { + return new LoadBalancedRetryFactory() { + @Override + public BackOffPolicy createBackOffPolicy(String service) { + return new ExponentialBackOffPolicy(); + } + }; + } +} +
    +
    +Configuration +When you use Ribbon with Spring Retry, you can control the retry functionality by configuring certain Ribbon properties. +To do so, set the client.ribbon.MaxAutoRetries, client.ribbon.MaxAutoRetriesNextServer, and client.ribbon.OkToRetryOnAllOperations properties. +See the Ribbon documentation for a description of what these properties do. + +Enabling client.ribbon.OkToRetryOnAllOperations includes retrying POST requests, which can have an impact +on the server’s resources, due to the buffering of the request body. + +In addition, you may want to retry requests when certain status codes are returned in the response. +You can list the response codes you would like the Ribbon client to retry by setting the clientName.ribbon.retryableStatusCodes property, as shown in the following example: +clientName: + ribbon: + retryableStatusCodes: 404,502 +You can also create a bean of type LoadBalancedRetryPolicy and implement the retryableStatusCode method to retry a request given the status code. +
    +Zuul +You can turn off Zuul’s retry functionality by setting zuul.retryable to false. +You can also disable retry functionality on a route-by-route basis by setting zuul.routes.routename.retryable to false. +
    +
    +
    + +HTTP Clients +Spring Cloud Netflix automatically creates the HTTP client used by Ribbon, Feign, and Zuul for you. +However, you can also provide your own HTTP clients customized as you need them to be. +To do so, you can create a bean of type ClosableHttpClient if you +are using the Apache Http Cient or OkHttpClient if you are using OK HTTP. + +When you create your own HTTP client, you are also responsible for implementing the correct connection management strategies for these clients. +Doing so improperly can result in resource management issues. + + + +Modules In Maintenance Mode +Placing a module in maintenance mode means that the Spring Cloud team will no longer be adding new features to the module. +We will fix blocker bugs and security issues, and we will also consider and review small pull requests from the community. +We intend to continue to support these modules for a period of at least a year from the general availability +of the Greenwich release train. +The following Spring Cloud Netflix modules and corresponding starters will be placed into maintenance mode: + + +spring-cloud-netflix-archaius + + +spring-cloud-netflix-hystrix-contract + + +spring-cloud-netflix-hystrix-dashboard + + +spring-cloud-netflix-hystrix-stream + + +spring-cloud-netflix-hystrix + + +spring-cloud-netflix-ribbon + + +spring-cloud-netflix-turbine-stream + + +spring-cloud-netflix-turbine + + +spring-cloud-netflix-zuul + + + +This does not include the Eureka or concurrency-limits modules. + + +
    + +Spring Cloud OpenFeign + +Greenwich.SR5 +This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms. + + +Declarative REST Client: Feign +Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign. +
    +How to Include Feign +To include Feign in your project use the starter with group org.springframework.cloud +and artifact id spring-cloud-starter-openfeign. See the Spring Cloud Project page +for details on setting up your build system with the current Spring Cloud Release Train. +Example spring boot app +@SpringBootApplication +@EnableFeignClients +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} + +StoreClient.java + +@FeignClient("stores") +public interface StoreClient { + @RequestMapping(method = RequestMethod.GET, value = "/stores") + List<Store> getStores(); + + @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") + Store update(@PathVariable("storeId") Long storeId, Store store); +} + + +In the @FeignClient annotation the String value ("stores" above) is +an arbitrary client name, which is used to create a Ribbon load +balancer (see below for details of Ribbon +support). You can also specify a URL using the url attribute +(absolute value or just a hostname). The name of the bean in the +application context is the fully qualified name of the interface. +To specify your own alias value you can use the qualifier value +of the @FeignClient annotation. +The Ribbon client above will want to discover the physical addresses +for the "stores" service. If your application is a Eureka client then +it will resolve the service in the Eureka service registry. If you +don’t want to use Eureka, you can simply configure a list of servers +in your external configuration (see +above for example). +
    +
    +Overriding Feign Defaults +A central concept in Spring Cloud’s Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the @FeignClient annotation. Spring Cloud creates a new ensemble as an +ApplicationContext on demand for each named client using FeignClientsConfiguration. This contains (amongst other things) an feign.Decoder, a feign.Encoder, and a feign.Contract. +It is possible to override the name of that ensemble by using the contextId +attribute of the @FeignClient annotation. +Spring Cloud lets you take full control of the feign client by declaring additional configuration (on top of the FeignClientsConfiguration) using @FeignClient. Example: +@FeignClient(name = "stores", configuration = FooConfiguration.class) +public interface StoreClient { + //.. +} +In this case the client is composed from the components already in FeignClientsConfiguration together with any in FooConfiguration (where the latter will override the former). + +FooConfiguration does not need to be annotated with @Configuration. However, if it is, then take care to exclude it from any @ComponentScan that would otherwise include this configuration as it will become the default source for feign.Decoder, feign.Encoder, feign.Contract, etc., when specified. This can be avoided by putting it in a separate, non-overlapping package from any @ComponentScan or @SpringBootApplication, or it can be explicitly excluded in @ComponentScan. + + +The serviceId attribute is now deprecated in favor of the name attribute. + + +Using contextId attribute of the @FeignClient annotation in addition to changing the name of +the ApplicationContext ensemble, it will override the alias of the client name +and it will be used as part of the name of the configuration bean created for that client. + + +Previously, using the url attribute, did not require the name attribute. Using name is now required. + +Placeholders are supported in the name and url attributes. +@FeignClient(name = "${feign.name}", url = "${feign.url}") +public interface StoreClient { + //.. +} +Spring Cloud Netflix provides the following beans by default for feign (BeanType beanName: ClassName): + + +Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder) + + +Encoder feignEncoder: SpringEncoder + + +Logger feignLogger: Slf4jLogger + + +Contract feignContract: SpringMvcContract + + +Feign.Builder feignBuilder: HystrixFeign.Builder + + +Client feignClient: if Ribbon is enabled it is a LoadBalancerFeignClient, otherwise the default feign client is used. + + +The OkHttpClient and ApacheHttpClient feign clients can be used by setting feign.okhttp.enabled or feign.httpclient.enabled to true, respectively, and having them on the classpath. +You can customize the HTTP client used by providing a bean of either ClosableHttpClient when using Apache or OkHttpClient when using OK HTTP. +Spring Cloud Netflix does not provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client: + + +Logger.Level + + +Retryer + + +ErrorDecoder + + +Request.Options + + +Collection<RequestInterceptor> + + +SetterFactory + + +Creating a bean of one of those type and placing it in a @FeignClient configuration (such as FooConfiguration above) allows you to override each one of the beans described. Example: +@Configuration +public class FooConfiguration { + @Bean + public Contract feignContract() { + return new feign.Contract.Default(); + } + + @Bean + public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { + return new BasicAuthRequestInterceptor("user", "password"); + } +} +This replaces the SpringMvcContract with feign.Contract.Default and adds a RequestInterceptor to the collection of RequestInterceptor. +@FeignClient also can be configured using configuration properties. +application.yml +feign: + client: + config: + feignName: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: full + errorDecoder: com.example.SimpleErrorDecoder + retryer: com.example.SimpleRetryer + requestInterceptors: + - com.example.FooRequestInterceptor + - com.example.BarRequestInterceptor + decode404: false + encoder: com.example.SimpleEncoder + decoder: com.example.SimpleDecoder + contract: com.example.SimpleContract +Default configurations can be specified in the @EnableFeignClients attribute defaultConfiguration in a similar manner as described above. The difference is that this configuration will apply to all feign clients. +If you prefer using configuration properties to configured all @FeignClient, you can create configuration properties with default feign name. +application.yml +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: basic +If we create both @Configuration bean and configuration properties, configuration properties will win. +It will override @Configuration values. But if you want to change the priority to @Configuration, +you can change feign.client.default-to-properties to false. + +If you need to use ThreadLocal bound variables in your RequestInterceptor`s you will need to either set the +thread isolation strategy for Hystrix to `SEMAPHORE or disable Hystrix in Feign. + +application.yml +# To disable Hystrix in Feign +feign: + hystrix: + enabled: false + +# To set thread isolation to SEMAPHORE +hystrix: + command: + default: + execution: + isolation: + strategy: SEMAPHORE +If we want to create multiple feign clients with the same name or url +so that they would point to the same server but each with a different custom configuration then +we have to use contextId attribute of the @FeignClient in order to avoid name +collision of these configuration beans. +@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class) +public interface FooClient { + //.. +} +@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class) +public interface BarClient { + //.. +} +
    +
    +Creating Feign Clients Manually +In some cases it might be necessary to customize your Feign Clients in a way that is not +possible using the methods above. In this case you can create Clients using the +Feign Builder API. Below is an example +which creates two Feign Clients with the same interface but configures each one with +a separate request interceptor. +@Import(FeignClientsConfiguration.class) +class FooController { + + private FooClient fooClient; + + private FooClient adminClient; + + @Autowired + public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) { + this.fooClient = Feign.builder().client(client) + .encoder(encoder) + .decoder(decoder) + .contract(contract) + .requestInterceptor(new BasicAuthRequestInterceptor("user", "user")) + .target(FooClient.class, "http://PROD-SVC"); + + this.adminClient = Feign.builder().client(client) + .encoder(encoder) + .decoder(decoder) + .contract(contract) + .requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin")) + .target(FooClient.class, "http://PROD-SVC"); + } +} + +In the above example FeignClientsConfiguration.class is the default configuration +provided by Spring Cloud Netflix. + + +PROD-SVC is the name of the service the Clients will be making requests to. + + +The Feign Contract object defines what annotations and values are valid on interfaces. The +autowired Contract bean provides supports for SpringMVC annotations, instead of +the default Feign native annotations. + +
    +
    +Feign Hystrix Support +If Hystrix is on the classpath and feign.hystrix.enabled=true, Feign will wrap all methods with a circuit breaker. Returning a com.netflix.hystrix.HystrixCommand is also available. This lets you use reactive patterns (with a call to .toObservable() or .observe() or asynchronous use (with a call to .queue()). +To disable Hystrix support on a per-client basis create a vanilla Feign.Builder with the "prototype" scope, e.g.: +@Configuration +public class FooConfiguration { + @Bean + @Scope("prototype") + public Feign.Builder feignBuilder() { + return Feign.builder(); + } +} + +Prior to the Spring Cloud Dalston release, if Hystrix was on the classpath Feign would have wrapped +all methods in a circuit breaker by default. This default behavior was changed in Spring Cloud Dalston in +favor for an opt-in approach. + +
    +
    +Feign Hystrix Fallbacks +Hystrix supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error. To enable fallbacks for a given @FeignClient set the fallback attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean. +@FeignClient(name = "hello", fallback = HystrixClientFallback.class) +protected interface HystrixClient { + @RequestMapping(method = RequestMethod.GET, value = "/hello") + Hello iFailSometimes(); +} + +static class HystrixClientFallback implements HystrixClient { + @Override + public Hello iFailSometimes() { + return new Hello("fallback"); + } +} +If one needs access to the cause that made the fallback trigger, one can use the fallbackFactory attribute inside @FeignClient. +@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class) +protected interface HystrixClient { + @RequestMapping(method = RequestMethod.GET, value = "/hello") + Hello iFailSometimes(); +} + +@Component +static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> { + @Override + public HystrixClient create(Throwable cause) { + return new HystrixClient() { + @Override + public Hello iFailSometimes() { + return new Hello("fallback; reason was: " + cause.getMessage()); + } + }; + } +} + +There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return com.netflix.hystrix.HystrixCommand and rx.Observable. + +
    +
    +Feign and <literal>@Primary</literal> +When using Feign with Hystrix fallbacks, there are multiple beans in the ApplicationContext of the same type. This will cause @Autowired to not work because there isn’t exactly one bean, or one marked as primary. To work around this, Spring Cloud Netflix marks all Feign instances as @Primary, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the primary attribute of @FeignClient to false. +@FeignClient(name = "hello", primary = false) +public interface HelloClient { + // methods here +} +
    +
    +Feign Inheritance Support +Feign supports boilerplate apis via single-inheritance interfaces. +This allows grouping common operations into convenient base interfaces. + +UserService.java + +public interface UserService { + + @RequestMapping(method = RequestMethod.GET, value ="/users/{id}") + User getUser(@PathVariable("id") long id); +} + + + +UserResource.java + +@RestController +public class UserResource implements UserService { + +} + + + +UserClient.java + +package project.user; + +@FeignClient("users") +public interface UserClient extends UserService { + +} + + + +It is generally not advisable to share an interface between a +server and a client. It introduces tight coupling, and also actually +doesn’t work with Spring MVC in its current form (method parameter +mapping is not inherited). + +
    +
    +Feign request/response compression +You may consider enabling the request or response GZIP compression for your +Feign requests. You can do this by enabling one of the properties: +feign.compression.request.enabled=true +feign.compression.response.enabled=true +Feign request compression gives you settings similar to what you may set for your web server: +feign.compression.request.enabled=true +feign.compression.request.mime-types=text/xml,application/xml,application/json +feign.compression.request.min-request-size=2048 +These properties allow you to be selective about the compressed media types and minimum request threshold length. +
    +
    +Feign logging +A logger is created for each Feign client created. By default the name of the logger is the full class name of the interface used to create the Feign client. Feign logging only responds to the DEBUG level. + +application.yml + +logging.level.project.user.UserClient: DEBUG + + +The Logger.Level object that you may configure per client, tells Feign how much to log. Choices are: + + +NONE, No logging (DEFAULT). + + +BASIC, Log only the request method and URL and the response status code and execution time. + + +HEADERS, Log the basic information along with request and response headers. + + +FULL, Log the headers, body, and metadata for both requests and responses. + + +For example, the following would set the Logger.Level to FULL: +@Configuration +public class FooConfiguration { + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} +
    +
    +Feign @QueryMap support +The OpenFeign @QueryMap annotation provides support for POJOs to be used as +GET parameter maps. Unfortunately, the default OpenFeign QueryMap annotation is +incompatible with Spring because it lacks a value property. +Spring Cloud OpenFeign provides an equivalent @SpringQueryMap annotation, which +is used to annotate a POJO or Map parameter as a query parameter map. +For example, the Params class defines parameters param1 and param2: +// Params.java +public class Params { + private String param1; + private String param2; + + // [Getters and setters omitted for brevity] +} +The following feign client uses the Params class by using the @SpringQueryMap annotation: +@FeignClient("demo") +public class DemoTemplate { + + @GetMapping(path = "/demo") + String demoEndpoint(@SpringQueryMap Params params); +} +
    +
    +Troubleshooting +
    +Early Initialization Errors +Depending on how you are using your Feign clients you may see initialization errors when starting your application. +To work around this problem you can use an ObjectProvider when autowiring your client. +@Autowired +ObjectProvider<TestFeginClient> testFeginClient; +
    +
    +
    +
    + +Spring Cloud Stream + +A Brief History of Spring’s Data Integration Journey +Spring’s journey on Data Integration started with Spring Integration. With its programming model, it provided a consistent developer experience to build applications that can embrace Enterprise Integration Patterns to connect with external systems such as, databases, message brokers, and among others. +Fast forward to the cloud-era, where microservices have become prominent in the enterprise setting. Spring Boot transformed the way how developers built Applications. With Spring’s programming model and the runtime responsibilities handled by Spring Boot, it became seamless to develop stand-alone, production-grade Spring-based microservices. +To extend this to Data Integration workloads, Spring Integration and Spring Boot were put together into a new project. Spring Cloud Stream was born. +With Spring Cloud Stream, developers can: +* Build, test, iterate, and deploy data-centric applications in isolation. +* Apply modern microservices architecture patterns, including composition through messaging. +* Decouple application responsibilities with event-centric thinking. An event can represent something that has happened in time, to which the downstream consumer applications can react without knowing where it originated or the producer’s identity. +* Port the business logic onto message brokers (such as RabbitMQ, Apache Kafka, Amazon Kinesis). +* Interoperate between channel-based and non-channel-based application binding scenarios to support stateless and stateful computations by using Project Reactor’s Flux and Kafka Streams APIs. +* Rely on the framework’s automatic content-type support for common use-cases. Extending to different data conversion types is possible. + + +Quick Start +You can try Spring Cloud Stream in less then 5 min even before you jump into any details by following this three-step guide. +We show you how to create a Spring Cloud Stream application that receives messages coming from the messaging middleware of your choice (more on this later) and logs received messages to the console. +We call it LoggingConsumer. +While not very practical, it provides a good introduction to some of the main concepts +and abstractions, making it easier to digest the rest of this user guide. +The three steps are as follows: + + + + + + + + + + + +
    +Creating a Sample Application by Using Spring Initializr +To get started, visit the Spring Initializr. From there, you can generate our LoggingConsumer application. To do so: + + +In the Dependencies section, start typing stream. +When the Cloud Stream option should appears, select it. + + +Start typing either 'kafka' or 'rabbit'. + + +Select Kafka or RabbitMQ. +Basically, you choose the messaging middleware to which your application binds. +We recommend using the one you have already installed or feel more comfortable with installing and running. +Also, as you can see from the Initilaizer screen, there are a few other options you can choose. +For example, you can choose Gradle as your build tool instead of Maven (the default). + + +In the Artifact field, type 'logging-consumer'. +The value of the Artifact field becomes the application name. +If you chose RabbitMQ for the middleware, your Spring Initializr should now be as follows: + + + + + +stream initializr + + + + +Click the Generate Project button. +Doing so downloads the zipped version of the generated project to your hard drive. + + +Unzip the file into the folder you want to use as your project directory. + + + +We encourage you to explore the many possibilities available in the Spring Initializr. +It lets you create many different kinds of Spring applications. + +
    +
    +Importing the Project into Your IDE +Now you can import the project into your IDE. +Keep in mind that, depending on the IDE, you may need to follow a specific import procedure. +For example, depending on how the project was generated (Maven or Gradle), you may need to follow specific import procedure (for example, in Eclipse or STS, you need to use File → Import → Maven → Existing Maven Project). +Once imported, the project must have no errors of any kind. Also, src/main/java should contain com.example.loggingconsumer.LoggingConsumerApplication. +Technically, at this point, you can run the application’s main class. +It is already a valid Spring Boot application. +However, it does not do anything, so we want to add some code. +
    +
    +Adding a Message Handler, Building, and Running +Modify the com.example.loggingconsumer.LoggingConsumerApplication class to look as follows: +@SpringBootApplication +@EnableBinding(Sink.class) +public class LoggingConsumerApplication { + + public static void main(String[] args) { + SpringApplication.run(LoggingConsumerApplication.class, args); + } + + @StreamListener(Sink.INPUT) + public void handle(Person person) { + System.out.println("Received: " + person); + } + + public static class Person { + private String name; + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String toString() { + return this.name; + } + } +} +As you can see from the preceding listing: + + +We have enabled Sink binding (input-no-output) by using @EnableBinding(Sink.class). +Doing so signals to the framework to initiate binding to the messaging middleware, where it automatically creates the destination (that is, queue, topic, and others) that are bound to the Sink.INPUT channel. + + +We have added a handler method to receive incoming messages of type Person. +Doing so lets you see one of the core features of the framework: It tries to automatically convert incoming message payloads to type Person. + + +You now have a fully functional Spring Cloud Stream application that does listens for messages. +From here, for simplicity, we assume you selected RabbitMQ in step one. +Assuming you have RabbitMQ installed and running, you can start the application by running its main method in your IDE. +You should see following output: + --- [ main] c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg, bound to: input + --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [localhost:5672] + --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#2a3a299:0/SimpleConnection@66c83fc8. . . + . . . + --- [ main] o.s.i.a.i.AmqpInboundChannelAdapter : started inbound.input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg + . . . + --- [ main] c.e.l.LoggingConsumerApplication : Started LoggingConsumerApplication in 2.531 seconds (JVM running for 2.897) +Go to the RabbitMQ management console or any other RabbitMQ client and send a message to input.anonymous.CbMIwdkJSBO1ZoPDOtHtCg. +The anonymous.CbMIwdkJSBO1ZoPDOtHtCg part represents the group name and is generated, so it is bound to be different in your environment. +For something more predictable, you can use an explicit group name by setting spring.cloud.stream.bindings.input.group=hello (or whatever name you like). +The contents of the message should be a JSON representation of the Person class, as follows: +{"name":"Sam Spade"} +Then, in your console, you should see: +Received: Sam Spade +You can also build and package your application into a boot jar (by using ./mvnw clean install) and run the built JAR by using the java -jar command. +Now you have a working (albeit very basic) Spring Cloud Stream application. +
    +
    + +What’s New in 2.0? +Spring Cloud Stream introduces a number of new features, enhancements, and changes. The following sections outline the most notable ones: + + + + + + + + +
    +New Features and Components + + +Polling Consumers: Introduction of polled consumers, which lets the application control message processing rates. +See for more details. +You can also read this blog post for more details. + + +Micrometer Support: Metrics has been switched to use Micrometer. +MeterRegistry is also provided as a bean so that custom applications can autowire it to capture custom metrics. +See for more details. + + +New Actuator Binding Controls: New actuator binding controls let you both visualize and control the Bindings lifecycle. +For more details, see . + + +Configurable RetryTemplate: Aside from providing properties to configure RetryTemplate, we now let you provide your own template, effectively overriding the one provided by the framework. +To use it, configure it as a @Bean in your application. + + +
    +
    +Notable Enhancements +This version includes the following notable enhancements: + + + + + + + + + + + +
    +Both Actuator and Web Dependencies Are Now Optional +This change slims down the footprint of the deployed application in the event neither actuator nor web dependencies required. +It also lets you switch between the reactive and conventional web paradigms by manually adding one of the following dependencies. +The following listing shows how to add the conventional web framework: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> +</dependency> +The following listing shows how to add the reactive web framework: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> +</dependency> +The following list shows how to add the actuator dependency: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> +</dependency> +
    +
    +Content-type Negotiation Improvements +One of the core themes for verion 2.0 is improvements (in both consistency and performance) around content-type negotiation and message conversion. +The following summary outlines the notable changes and improvements in this area. +See the section for more details. +Also this blog post contains more detail. + + +All message conversion is now handled only by MessageConverter objects. + + +We introduced the @StreamMessageConverter annotation to provide custom MessageConverter objects. + + +We introduced the default Content Type as application/json, which needs to be taken into consideration when migrating 1.3 application or operating in the mixed mode (that is, 1.3 producer → 2.0 consumer). + + +Messages with textual payloads and a contentType of text/…​ or …​/json are no longer converted to Message<String> for cases where the argument type of the provided MessageHandler can not be determined (that is, public void handle(Message<?> message) or public void handle(Object payload)). +Furthermore, a strong argument type may not be enough to properly convert messages, so the contentType header may be used as a supplement by some MessageConverters. + + +
    +
    +
    +Notable Deprecations +As of version 2.0, the following items have been deprecated: + + + + + + + + +
    +Java Serialization (Java Native and Kryo) +JavaSerializationMessageConverter and KryoMessageConverter remain for now. However, we plan to move them out of the core packages and support in the future. +The main reason for this deprecation is to flag the issue that type-based, language-specific serialization could cause in distributed environments, where Producers and Consumers may depend on different JVM versions or have different versions of supporting libraries (that is, Kryo). +We also wanted to draw the attention to the fact that Consumers and Producers may not even be Java-based, so polyglot style serialization (i.e., JSON) is better suited. +
    +
    +Deprecated Classes and Methods +The following is a quick summary of notable deprecations. See the corresponding {spring-cloud-stream-javadoc-current}[javadoc] for more details. + + +SharedChannelRegistry. Use SharedBindingTargetRegistry. + + +Bindings. +Beans qualified by it are already uniquely identified by their type — for example, provided Source, Processor, or custom bindings: + + +public interface Sample { + String OUTPUT = "sampleOutput"; + + @Output(Sample.OUTPUT) + MessageChannel output(); +} + + +HeaderMode.raw. Use none, headers or embeddedHeaders + + +ProducerProperties.partitionKeyExtractorClass in favor of partitionKeyExtractorName and ProducerProperties.partitionSelectorClass in favor of partitionSelectorName. +This change ensures that both components are Spring configured and managed and are referenced in a Spring-friendly way. + + +BinderAwareRouterBeanPostProcessor. While the component remains, it is no longer a BeanPostProcessor and will be renamed in the future. + + +BinderProperties.setEnvironment(Properties environment). Use BinderProperties.setEnvironment(Map<String, Object> environment). + + +This section goes into more detail about how you can work with Spring Cloud Stream. +It covers topics such as creating and running stream applications. +
    +
    +
    + +Introducing Spring Cloud Stream +Spring Cloud Stream is a framework for building message-driven microservice applications. +Spring Cloud Stream builds upon Spring Boot to create standalone, production-grade Spring applications and uses Spring Integration to provide connectivity to message brokers. +It provides opinionated configuration of middleware from several vendors, introducing the concepts of persistent publish-subscribe semantics, consumer groups, and partitions. +You can add the @EnableBinding annotation to your application to get immediate connectivity to a message broker, and you can add @StreamListener to a method to cause it to receive events for stream processing. +The following example shows a sink application that receives external messages: +@SpringBootApplication +@EnableBinding(Sink.class) +public class VoteRecordingSinkApplication { + + public static void main(String[] args) { + SpringApplication.run(VoteRecordingSinkApplication.class, args); + } + + @StreamListener(Sink.INPUT) + public void processVote(Vote vote) { + votingService.recordVote(vote); + } +} +The @EnableBinding annotation takes one or more interfaces as parameters (in this case, the parameter is a single Sink interface). +An interface declares input and output channels. +Spring Cloud Stream provides the Source, Sink, and Processor interfaces. You can also define your own interfaces. +The following listing shows the definition of the Sink interface: +public interface Sink { + String INPUT = "input"; + + @Input(Sink.INPUT) + SubscribableChannel input(); +} +The @Input annotation identifies an input channel, through which received messages enter the application. +The @Output annotation identifies an output channel, through which published messages leave the application. +The @Input and @Output annotations can take a channel name as a parameter. +If a name is not provided, the name of the annotated method is used. +Spring Cloud Stream creates an implementation of the interface for you. +You can use this in the application by autowiring it, as shown in the following example (from a test case): +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = VoteRecordingSinkApplication.class) +@WebAppConfiguration +@DirtiesContext +public class StreamApplicationTests { + + @Autowired + private Sink sink; + + @Test + public void contextLoads() { + assertNotNull(this.sink.input()); + } +} + + +Main Concepts +Spring Cloud Stream provides a number of abstractions and primitives that simplify the writing of message-driven microservice applications. +This section gives an overview of the following: + + +Spring Cloud Stream’s application model + + + + + +Persistent publish-subscribe support + + +Consumer group support + + +Partitioning support + + +A pluggable Binder SPI + + +
    +Application Model +A Spring Cloud Stream application consists of a middleware-neutral core. +The application communicates with the outside world through input and output channels injected into it by Spring Cloud Stream. +Channels are connected to external brokers through middleware-specific Binder implementations. +
    +Spring Cloud Stream Application + + + + +SCSt with binder + +
    +
    +Fat JAR +Spring Cloud Stream applications can be run in stand-alone mode from your IDE for testing. +To run a Spring Cloud Stream application in production, you can create an executable (or fat) JAR by using the standard Spring Boot tooling provided for Maven or Gradle. See the Spring Boot Reference Guide for more details. +
    +
    +
    +The Binder Abstraction +Spring Cloud Stream provides Binder implementations for Kafka and Rabbit MQ. +Spring Cloud Stream also includes a TestSupportBinder, which leaves a channel unmodified so that tests can interact with channels directly and reliably assert on what is received. +You can also use the extensible API to write your own Binder. +Spring Cloud Stream uses Spring Boot for configuration, and the Binder abstraction makes it possible for a Spring Cloud Stream application to be flexible in how it connects to middleware. +For example, deployers can dynamically choose, at runtime, the destinations (such as the Kafka topics or RabbitMQ exchanges) to which channels connect. +Such configuration can be provided through external configuration properties and in any form supported by Spring Boot (including application arguments, environment variables, and application.yml or application.properties files). +In the sink example from the section, setting the spring.cloud.stream.bindings.input.destination application property to raw-sensor-data causes it to read from the raw-sensor-data Kafka topic or from a queue bound to the raw-sensor-data RabbitMQ exchange. +Spring Cloud Stream automatically detects and uses a binder found on the classpath. +You can use different types of middleware with the same code. +To do so, include a different binder at build time. +For more complex use cases, you can also package multiple binders with your application and have it choose the binder( and even whether to use different binders for different channels) at runtime. +
    +
    +Persistent Publish-Subscribe Support +Communication between applications follows a publish-subscribe model, where data is broadcast through shared topics. +This can be seen in the following figure, which shows a typical deployment for a set of interacting Spring Cloud Stream applications. +
    +Spring Cloud Stream Publish-Subscribe + + + + +SCSt sensors + +
    +Data reported by sensors to an HTTP endpoint is sent to a common destination named raw-sensor-data. +From the destination, it is independently processed by a microservice application that computes time-windowed averages and by another microservice application that ingests the raw data into HDFS (Hadoop Distributed File System). +In order to process the data, both applications declare the topic as their input at runtime. +The publish-subscribe communication model reduces the complexity of both the producer and the consumer and lets new applications be added to the topology without disruption of the existing flow. +For example, downstream from the average-calculating application, you can add an application that calculates the highest temperature values for display and monitoring. +You can then add another application that interprets the same flow of averages for fault detection. +Doing all communication through shared topics rather than point-to-point queues reduces coupling between microservices. +While the concept of publish-subscribe messaging is not new, Spring Cloud Stream takes the extra step of making it an opinionated choice for its application model. +By using native middleware support, Spring Cloud Stream also simplifies use of the publish-subscribe model across different platforms. +
    +
    +Consumer Groups +While the publish-subscribe model makes it easy to connect applications through shared topics, the ability to scale up by creating multiple instances of a given application is equally important. +When doing so, different instances of an application are placed in a competing consumer relationship, where only one of the instances is expected to handle a given message. +Spring Cloud Stream models this behavior through the concept of a consumer group. +(Spring Cloud Stream consumer groups are similar to and inspired by Kafka consumer groups.) +Each consumer binding can use the spring.cloud.stream.bindings.<channelName>.group property to specify a group name. +For the consumers shown in the following figure, this property would be set as spring.cloud.stream.bindings.<channelName>.group=hdfsWrite or spring.cloud.stream.bindings.<channelName>.group=average. +
    +Spring Cloud Stream Consumer Groups + + + + +SCSt groups + +
    +All groups that subscribe to a given destination receive a copy of published data, but only one member of each group receives a given message from that destination. +By default, when a group is not specified, Spring Cloud Stream assigns the application to an anonymous and independent single-member consumer group that is in a publish-subscribe relationship with all other consumer groups. +
    +
    +Consumer Types +Two types of consumer are supported: + + +Message-driven (sometimes referred to as Asynchronous) + + +Polled (sometimes referred to as Synchronous) + + +Prior to version 2.0, only asynchronous consumers were supported. A message is delivered as soon as it is available and a thread is available to process it. +When you wish to control the rate at which messages are processed, you might want to use a synchronous consumer. +
    +Durability +Consistent with the opinionated application model of Spring Cloud Stream, consumer group subscriptions are durable. +That is, a binder implementation ensures that group subscriptions are persistent and that, once at least one subscription for a group has been created, the group receives messages, even if they are sent while all applications in the group are stopped. + +Anonymous subscriptions are non-durable by nature. +For some binder implementations (such as RabbitMQ), it is possible to have non-durable group subscriptions. + +In general, it is preferable to always specify a consumer group when binding an application to a given destination. +When scaling up a Spring Cloud Stream application, you must specify a consumer group for each of its input bindings. +Doing so prevents the application’s instances from receiving duplicate messages (unless that behavior is desired, which is unusual). +
    +
    +
    +Partitioning Support +Spring Cloud Stream provides support for partitioning data between multiple instances of a given application. +In a partitioned scenario, the physical communication medium (such as the broker topic) is viewed as being structured into multiple partitions. +One or more producer application instances send data to multiple consumer application instances and ensure that data identified by common characteristics are processed by the same consumer instance. +Spring Cloud Stream provides a common abstraction for implementing partitioned processing use cases in a uniform fashion. +Partitioning can thus be used whether the broker itself is naturally partitioned (for example, Kafka) or not (for example, RabbitMQ). +
    +Spring Cloud Stream Partitioning + + + + +SCSt partitioning + +
    +Partitioning is a critical concept in stateful processing, where it is critical (for either performance or consistency reasons) to ensure that all related data is processed together. +For example, in the time-windowed average calculation example, it is important that all measurements from any given sensor are processed by the same application instance. + +To set up a partitioned processing scenario, you must configure both the data-producing and the data-consuming ends. + +
    +
    + +Programming Model +To understand the programming model, you should be familiar with the following core concepts: + + +Destination Binders: Components responsible to provide integration with the external messaging systems. + + +Destination Bindings: Bridge between the external messaging systems and application provided Producers and Consumers of messages (created by the Destination Binders). + + +Message: The canonical data structure used by producers and consumers to communicate with Destination Binders (and thus other applications via external messaging systems). + + + + + + + +SCSt overview + + +
    +Destination Binders +Destination Binders are extension components of Spring Cloud Stream responsible for providing the necessary configuration and implementation to facilitate +integration with external messaging systems. +This integration is responsible for connectivity, delegation, and routing of messages to and from producers and consumers, data type conversion, +invocation of the user code, and more. +Binders handle a lot of the boiler plate responsibilities that would otherwise fall on your shoulders. However, to accomplish that, the binder still needs +some help in the form of minimalistic yet required set of instructions from the user, which typically come in the form of some type of configuration. +While it is out of scope of this section to discuss all of the available binder and binding configuration options (the rest of the manual covers them extensively), +Destination Binding does require special attention. The next section discusses it in detail. +
    +
    +Destination Bindings +As stated earlier, Destination Bindings provide a bridge between the external messaging system and application-provided Producers and Consumers. +Applying the @EnableBinding annotation to one of the application’s configuration classes defines a destination binding. +The @EnableBinding annotation itself is meta-annotated with @Configuration and triggers the configuration of the Spring Cloud Stream infrastructure. +The following example shows a fully configured and functioning Spring Cloud Stream application that receives the payload of the message from the INPUT +destination as a String type (see section), logs it to the console and sends it to the OUTPUT destination after converting it to upper case. +@SpringBootApplication +@EnableBinding(Processor.class) +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + + @StreamListener(Processor.INPUT) + @SendTo(Processor.OUTPUT) + public String handle(String value) { + System.out.println("Received: " + value); + return value.toUpperCase(); + } +} +As you can see the @EnableBinding annotation can take one or more interface classes as parameters. The parameters are referred to as bindings, +and they contain methods representing bindable components. +These components are typically message channels (see Spring Messaging) +for channel-based binders (such as Rabbit, Kafka, and others). However other types of bindings can +provide support for the native features of the corresponding technology. For example Kafka Streams binder (formerly known as KStream) allows native bindings directly to Kafka Streams +(see Kafka Streams for more details). +Spring Cloud Stream already provides binding interfaces for typical message exchange contracts, which include: + + +Sink: Identifies the contract for the message consumer by providing the destination from which the message is consumed. + + +Source: Identifies the contract for the message producer by providing the destination to which the produced message is sent. + + +Processor: Encapsulates both the sink and the source contracts by exposing two destinations that allow consumption and production of messages. + + +public interface Sink { + + String INPUT = "input"; + + @Input(Sink.INPUT) + SubscribableChannel input(); +} +public interface Source { + + String OUTPUT = "output"; + + @Output(Source.OUTPUT) + MessageChannel output(); +} +public interface Processor extends Source, Sink {} +While the preceding example satisfies the majority of cases, you can also define your own contracts by defining your own bindings interfaces and use @Input and @Output +annotations to identify the actual bindable components. +For example: +public interface Barista { + + @Input + SubscribableChannel orders(); + + @Output + MessageChannel hotDrinks(); + + @Output + MessageChannel coldDrinks(); +} +Using the interface shown in the preceding example as a parameter to @EnableBinding triggers the creation of the three bound channels named orders, hotDrinks, and coldDrinks, +respectively. +You can provide as many binding interfaces as you need, as arguments to the @EnableBinding annotation, as shown in the following example: +@EnableBinding(value = { Orders.class, Payment.class }) +In Spring Cloud Stream, the bindable MessageChannel components are the Spring Messaging MessageChannel (for outbound) and its extension, SubscribableChannel, +(for inbound). +Pollable Destination Binding +While the previously described bindings support event-based message consumption, sometimes you need more control, such as rate of consumption. +Starting with version 2.0, you can now bind a pollable consumer: +The following example shows how to bind a pollable consumer: +public interface PolledBarista { + + @Input + PollableMessageSource orders(); + . . . +} +In this case, an implementation of PollableMessageSource is bound to the orders “channel”. See for more details. +Customizing Channel Names +By using the @Input and @Output annotations, you can specify a customized channel name for the channel, as shown in the following example: +public interface Barista { + @Input("inboundOrders") + SubscribableChannel orders(); +} +In the preceding example, the created bound channel is named inboundOrders. +Normally, you need not access individual channels or bindings directly (other then configuring them via @EnableBinding annotation). However there may be +times, such as testing or other corner cases, when you do. +Aside from generating channels for each binding and registering them as Spring beans, for each bound interface, Spring Cloud Stream generates a bean that implements the interface. +That means you can have access to the interfaces representing the bindings or individual channels by auto-wiring either in your application, as shown in the following two examples: +Autowire Binding interface +@Autowire +private Source source + +public void sayHello(String name) { + source.output().send(MessageBuilder.withPayload(name).build()); +} +Autowire individual channel +@Autowire +private MessageChannel output; + +public void sayHello(String name) { + output.send(MessageBuilder.withPayload(name).build()); +} +You can also use standard Spring’s @Qualifier annotation for cases when channel names are customized or in multiple-channel scenarios that require specifically named channels. +The following example shows how to use the @Qualifier annotation in this way: +@Autowire +@Qualifier("myChannel") +private MessageChannel output; +
    +
    +Producing and Consuming Messages +You can write a Spring Cloud Stream application by using either Spring Integration annotations or Spring Cloud Stream native annotation. +
    +Spring Integration Support +Spring Cloud Stream is built on the concepts and patterns defined by Enterprise Integration Patterns and relies +in its internal implementation on an already established and popular implementation of Enterprise Integration Patterns within the Spring portfolio of projects: +Spring Integration framework. +So its only natural for it to support the foundation, semantics, and configuration options that are already established by Spring Integration +For example, you can attach the output channel of a Source to a MessageSource and use the familiar @InboundChannelAdapter annotation, as follows: +@EnableBinding(Source.class) +public class TimerSource { + + @Bean + @InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedDelay = "10", maxMessagesPerPoll = "1")) + public MessageSource<String> timerMessageSource() { + return () -> new GenericMessage<>("Hello Spring Cloud Stream"); + } +} +Similarly, you can use @Transformer or @ServiceActivator while providing an implementation of a message handler method for a Processor binding contract, as shown in the following example: +@EnableBinding(Processor.class) +public class TransformProcessor { + @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT) + public Object transform(String message) { + return message.toUpperCase(); + } +} + +While this may be skipping ahead a bit, it is important to understand that, when you consume from the same binding using @StreamListener annotation, a pub-sub model is used. +Each method annotated with @StreamListener receives its own copy of a message, and each one has its own consumer group. +However, if you consume from the same binding by using one of the Spring Integration annotation (such as @Aggregator, @Transformer, or @ServiceActivator), those consume in a competing model. +No individual consumer group is created for each subscription. + +
    +
    +Using @StreamListener Annotation +Complementary to its Spring Integration support, Spring Cloud Stream provides its own @StreamListener annotation, modeled after other Spring Messaging annotations +(@MessageMapping, @JmsListener, @RabbitListener, and others) and provides conviniences, such as content-based routing and others. +@EnableBinding(Sink.class) +public class VoteHandler { + + @Autowired + VotingService votingService; + + @StreamListener(Sink.INPUT) + public void handle(Vote vote) { + votingService.record(vote); + } +} +As with other Spring Messaging methods, method arguments can be annotated with @Payload, @Headers, and @Header. +For methods that return data, you must use the @SendTo annotation to specify the output binding destination for data returned by the method, as shown in the following example: +@EnableBinding(Processor.class) +public class TransformProcessor { + + @Autowired + VotingService votingService; + + @StreamListener(Processor.INPUT) + @SendTo(Processor.OUTPUT) + public VoteResult handle(Vote vote) { + return votingService.record(vote); + } +} +
    +
    +Using @StreamListener for Content-based routing +Spring Cloud Stream supports dispatching messages to multiple handler methods annotated with @StreamListener based on conditions. +In order to be eligible to support conditional dispatching, a method must satisfy the follow conditions: + + +It must not return a value. + + +It must be an individual message handling method (reactive API methods are not supported). + + +The condition is specified by a SpEL expression in the condition argument of the annotation and is evaluated for each message. +All the handlers that match the condition are invoked in the same thread, and no assumption must be made about the order in which the invocations take place. +In the following example of a @StreamListener with dispatching conditions, all the messages bearing a header type with the value bogey are dispatched to the +receiveBogey method, and all the messages bearing a header type with the value bacall are dispatched to the receiveBacall method. +@EnableBinding(Sink.class) +@EnableAutoConfiguration +public static class TestPojoWithAnnotatedArguments { + + @StreamListener(target = Sink.INPUT, condition = "headers['type']=='bogey'") + public void receiveBogey(@Payload BogeyPojo bogeyPojo) { + // handle the message + } + + @StreamListener(target = Sink.INPUT, condition = "headers['type']=='bacall'") + public void receiveBacall(@Payload BacallPojo bacallPojo) { + // handle the message + } +} +Content Type Negotiation in the Context of condition +It is important to understand some of the mechanics behind content-based routing using the condition argument of @StreamListener, especially in the context of the type of the message as a whole. +It may also help if you familiarize yourself with the before you proceed. +Consider the following scenario: +@EnableBinding(Sink.class) +@EnableAutoConfiguration +public static class CatsAndDogs { + + @StreamListener(target = Sink.INPUT, condition = "payload.class.simpleName=='Dog'") + public void bark(Dog dog) { + // handle the message + } + + @StreamListener(target = Sink.INPUT, condition = "payload.class.simpleName=='Cat'") + public void purr(Cat cat) { + // handle the message + } +} +The preceding code is perfectly valid. It compiles and deploys without any issues, yet it never produces the result you expect. +That is because you are testing something that does not yet exist in a state you expect. That is because the payload of the message is not yet converted from the +wire format (byte[]) to the desired type. +In other words, it has not yet gone through the type conversion process described in the . +So, unless you use a SPeL expression that evaluates raw data (for example, the value of the first byte in the byte array), use message header-based expressions +(such as condition = "headers['type']=='dog'"). + +At the moment, dispatching through @StreamListener conditions is supported only for channel-based binders (not for reactive programming) +support. + +
    +
    +Spring Cloud Function support +Since Spring Cloud Stream v2.1, another alternative for defining stream handlers and sources is to use build-in +support for Spring Cloud Function where they can be expressed as beans of + type java.util.function.[Supplier/Function/Consumer]. +To specify which functional bean to bind to the external destination(s) exposed by the bindings, you must provide spring.cloud.stream.function.definition property. +Here is the example of the Processor application exposing message handler as java.util.function.Function +@SpringBootApplication +@EnableBinding(Processor.class) +public class MyFunctionBootApp { + + public static void main(String[] args) { + SpringApplication.run(MyFunctionBootApp.class, "--spring.cloud.stream.function.definition=toUpperCase"); + } + + @Bean + public Function<String, String> toUpperCase() { + return s -> s.toUpperCase(); + } +} +In the above you we simply define a bean of type java.util.function.Function called toUpperCase and identify it as a bean to be used as message handler +whose 'input' and 'output' must be bound to the external destinations exposed by the Processor binding. +Below are the examples of simple functional applications to support Source, Processor and Sink. +Here is the example of a Source application defined as java.util.function.Supplier +@SpringBootApplication +@EnableBinding(Source.class) +public static class SourceFromSupplier { + public static void main(String[] args) { + SpringApplication.run(SourceFromSupplier.class, "--spring.cloud.stream.function.definition=date"); + } + @Bean + public Supplier<Date> date() { + return () -> new Date(12345L); + } +} +Here is the example of a Processor application defined as java.util.function.Function +@SpringBootApplication +@EnableBinding(Processor.class) +public static class ProcessorFromFunction { + public static void main(String[] args) { + SpringApplication.run(ProcessorFromFunction.class, "--spring.cloud.stream.function.definition=toUpperCase"); + } + @Bean + public Function<String, String> toUpperCase() { + return s -> s.toUpperCase(); + } +} +Here is the example of a Sink application defined as java.util.function.Consumer +@EnableAutoConfiguration +@EnableBinding(Sink.class) +public static class SinkFromConsumer { + public static void main(String[] args) { + SpringApplication.run(SinkFromConsumer.class, "--spring.cloud.stream.function.definition=sink"); + } + @Bean + public Consumer<String> sink() { + return System.out::println; + } +} +
    +Functional Composition +Using this programming model you can also benefit from functional composition where you can dynamically compose complex handlers from a set of simple functions. +As an example let’s add the following function bean to the application defined above +@Bean +public Function<String, String> wrapInQuotes() { + return s -> "\"" + s + "\""; +} +and modify the spring.cloud.stream.function.definition property to reflect your intention to compose a new function from both ‘toUpperCase’ and ‘wrapInQuotes’. +To do that Spring Cloud Function allows you to use | (pipe) symbol. So to finish our example our property will now look like this: +—spring.cloud.stream.function.definition=toUpperCase|wrapInQuotes +
    +
    +
    +Using Polled Consumers +
    +Overview +When using polled consumers, you poll the PollableMessageSource on demand. +Consider the following example of a polled consumer: +public interface PolledConsumer { + + @Input + PollableMessageSource destIn(); + + @Output + MessageChannel destOut(); + +} +Given the polled consumer in the preceding example, you might use it as follows: +@Bean +public ApplicationRunner poller(PollableMessageSource destIn, MessageChannel destOut) { + return args -> { + while (someCondition()) { + try { + if (!destIn.poll(m -> { + String newPayload = ((String) m.getPayload()).toUpperCase(); + destOut.send(new GenericMessage<>(newPayload)); + })) { + Thread.sleep(1000); + } + } + catch (Exception e) { + // handle failure + } + } + }; +} +The PollableMessageSource.poll() method takes a MessageHandler argument (often a lambda expression, as shown here). +It returns true if the message was received and successfully processed. +As with message-driven consumers, if the MessageHandler throws an exception, messages are published to error channels, as discussed in . +Normally, the poll() method acknowledges the message when the MessageHandler exits. +If the method exits abnormally, the message is rejected (not re-queued), but see . +You can override that behavior by taking responsibility for the acknowledgment, as shown in the following example: +@Bean +public ApplicationRunner poller(PollableMessageSource dest1In, MessageChannel dest2Out) { + return args -> { + while (someCondition()) { + if (!dest1In.poll(m -> { + StaticMessageHeaderAccessor.getAcknowledgmentCallback(m).noAutoAck(); + // e.g. hand off to another thread which can perform the ack + // or acknowledge(Status.REQUEUE) + + })) { + Thread.sleep(1000); + } + } + }; +} + +You must ack (or nack) the message at some point, to avoid resource leaks. + + +Some messaging systems (such as Apache Kafka) maintain a simple offset in a log. If a delivery fails and is re-queued with StaticMessageHeaderAccessor.getAcknowledgmentCallback(m).acknowledge(Status.REQUEUE);, any later successfully ack’d messages are redelivered. + +There is also an overloaded poll method, for which the definition is as follows: +poll(MessageHandler handler, ParameterizedTypeReference<?> type) +The type is a conversion hint that allows the incoming message payload to be converted, as shown in the following example: +boolean result = pollableSource.poll(received -> { + Map<String, Foo> payload = (Map<String, Foo>) received.getPayload(); + ... + + }, new ParameterizedTypeReference<Map<String, Foo>>() {}); +
    +
    +Handling Errors +By default, an error channel is configured for the pollable source; if the callback throws an exception, an ErrorMessage is sent to the error channel (<destination>.<group>.errors); this error channel is also bridged to the global Spring Integration errorChannel. +You can subscribe to either error channel with a @ServiceActivator to handle errors; without a subscription, the error will simply be logged and the message will be acknowledged as successful. +If the error channel service activator throws an exception, the message will be rejected (by default) and won’t be redelivered. +If the service activator throws a RequeueCurrentMessageException, the message will be requeued at the broker and will be again retrieved on a subsequent poll. +If the listener throws a RequeueCurrentMessageException directly, the message will be requeued, as discussed above, and will not be sent to the error channels. +
    +
    +
    +
    +Error Handling +Errors happen, and Spring Cloud Stream provides several flexible mechanisms to handle them. +The error handling comes in two flavors: + + +application: The error handling is done within the application (custom error handler). + + +system: The error handling is delegated to the binder (re-queue, DL, and others). Note that the techniques are dependent on binder implementation and the +capability of the underlying messaging middleware. + + +Spring Cloud Stream uses the Spring Retry library to facilitate successful message processing. See for more details. +However, when all fails, the exceptions thrown by the message handlers are propagated back to the binder. At that point, binder invokes custom error handler or communicates +the error back to the messaging system (re-queue, DLQ, and others). +
    +Application Error Handling +There are two types of application-level error handling. Errors can be handled at each binding subscription or a global handler can handle all the binding subscription errors. Let’s review the details. +
    +A Spring Cloud Stream Sink Application with Custom and Global Error Handlers + + + + +custom vs global error channels + +
    +For each input binding, Spring Cloud Stream creates a dedicated error channel with the following semantics <destinationName>.errors. + +The <destinationName> consists of the name of the binding (such as input) and the name of the group (such as myGroup). + +Consider the following: +spring.cloud.stream.bindings.input.group=myGroup +@StreamListener(Sink.INPUT) // destination name 'input.myGroup' +public void handle(Person value) { + throw new RuntimeException("BOOM!"); +} + +@ServiceActivator(inputChannel = Processor.INPUT + ".myGroup.errors") //channel name 'input.myGroup.errors' +public void error(Message<?> message) { + System.out.println("Handling ERROR: " + message); +} +In the preceding example the destination name is input.myGroup and the dedicated error channel name is input.myGroup.errors. + +The use of @StreamListener annotation is intended specifically to define bindings that bridge internal channels and external destinations. Given that the destination +specific error channel does NOT have an associated external destination, such channel is a prerogative of Spring Integration (SI). This means that the handler +for such destination must be defined using one of the SI handler annotations (i.e., @ServiceActivator, @Transformer etc.). + + +If group is not specified anonymous group is used (something like input.anonymous.2K37rb06Q6m2r51-SPIDDQ), which is not suitable for error +handling scenarious, since you don’t know what it’s going to be until the destination is created. + +Also, in the event you are binding to the existing destination such as: +spring.cloud.stream.bindings.input.destination=myFooDestination +spring.cloud.stream.bindings.input.group=myGroup +the full destination name is myFooDestination.myGroup and then the dedicated error channel name is myFooDestination.myGroup.errors. +Back to the example…​ +The handle(..) method, which subscribes to the channel named input, throws an exception. Given there is also a subscriber to the error channel input.myGroup.errors +all error messages are handled by this subscriber. +If you have multiple bindings, you may want to have a single error handler. Spring Cloud Stream automatically provides support for +a global error channel by bridging each individual error channel to the channel named errorChannel, allowing a single subscriber to handle all errors, +as shown in the following example: +@StreamListener("errorChannel") +public void error(Message<?> message) { + System.out.println("Handling ERROR: " + message); +} +This may be a convenient option if error handling logic is the same regardless of which handler produced the error. +
    +
    +System Error Handling +System-level error handling implies that the errors are communicated back to the messaging system and, given that not every messaging system +is the same, the capabilities may differ from binder to binder. +That said, in this section we explain the general idea behind system level error handling and use Rabbit binder as an example. NOTE: Kafka binder provides similar +support, although some configuration properties do differ. Also, for more details and configuration options, see the individual binder’s documentation. +If no internal error handlers are configured, the errors propagate to the binders, and the binders subsequently propagate those errors back to the messaging system. +Depending on the capabilities of the messaging system such a system may drop the message, re-queue the message for re-processing or send the failed message to DLQ. +Both Rabbit and Kafka support these concepts. However, other binders may not, so refer to your individual binder’s documentation for details on supported system-level +error-handling options. +
    +Drop Failed Messages +By default, if no additional system-level configuration is provided, the messaging system drops the failed message. +While acceptable in some cases, for most cases, it is not, and we need some recovery mechanism to avoid message loss. +
    +
    +DLQ - Dead Letter Queue +DLQ allows failed messages to be sent to a special destination: - Dead Letter Queue. +When configured, failed messages are sent to this destination for subsequent re-processing or auditing and reconciliation. +For example, continuing on the previous example and to set up the DLQ with Rabbit binder, you need to set the following property: +spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true +Keep in mind that, in the above property, input corresponds to the name of the input destination binding. +The consumer indicates that it is a consumer property and auto-bind-dlq instructs the binder to configure DLQ for input +destination, which results in an additional Rabbit queue named input.myGroup.dlq. +Once configured, all failed messages are routed to this queue with an error message similar to the following: +delivery_mode: 1 +headers: +x-death: +count: 1 +reason: rejected +queue: input.hello +time: 1522328151 +exchange: +routing-keys: input.myGroup +Payload {"name”:"Bob"} +As you can see from the above, your original message is preserved for further actions. +However, one thing you may have noticed is that there is limited information on the original issue with the message processing. For example, you do not see a stack +trace corresponding to the original error. +To get more relevant information about the original error, you must set an additional property: +spring.cloud.stream.rabbit.bindings.input.consumer.republish-to-dlq=true +Doing so forces the internal error handler to intercept the error message and add additional information to it before publishing it to DLQ. +Once configured, you can see that the error message contains more information relevant to the original error, as follows: +delivery_mode: 2 +headers: +x-original-exchange: +x-exception-message: has an error +x-original-routingKey: input.myGroup +x-exception-stacktrace: org.springframework.messaging.MessageHandlingException: nested exception is + org.springframework.messaging.MessagingException: has an error, failedMessage=GenericMessage [payload=byte[15], + headers={amqp_receivedDeliveryMode=NON_PERSISTENT, amqp_receivedRoutingKey=input.hello, amqp_deliveryTag=1, + deliveryAttempt=3, amqp_consumerQueue=input.hello, amqp_redelivered=false, id=a15231e6-3f80-677b-5ad7-d4b1e61e486e, + amqp_consumerTag=amq.ctag-skBFapilvtZhDsn0k3ZmQg, contentType=application/json, timestamp=1522327846136}] + at org.spring...integ...han...MethodInvokingMessageProcessor.processMessage(MethodInvokingMessageProcessor.java:107) + at. . . . . +Payload {"name”:"Bob"} +This effectively combines application-level and system-level error handling to further assist with downstream troubleshooting mechanics. +
    +
    +Re-queue Failed Messages +As mentioned earlier, the currently supported binders (Rabbit and Kafka) rely on RetryTemplate to facilitate successful message processing. See for details. +However, for cases when max-attempts property is set to 1, internal reprocessing of the message is disabled. At this point, you can facilitate message re-processing (re-tries) +by instructing the messaging system to re-queue the failed message. Once re-queued, the failed message is sent back to the original handler, essentially creating a retry loop. +This option may be feasible for cases where the nature of the error is related to some sporadic yet short-term unavailability of some resource. +To accomplish that, you must set the following properties: +spring.cloud.stream.bindings.input.consumer.max-attempts=1 +spring.cloud.stream.rabbit.bindings.input.consumer.requeue-rejected=true +In the preceding example, the max-attempts set to 1 essentially disabling internal re-tries and requeue-rejected (short for requeue rejected messages) is set to true. +Once set, the failed message is resubmitted to the same handler and loops continuously or until the handler throws AmqpRejectAndDontRequeueException +essentially allowing you to build your own re-try logic within the handler itself. +
    +
    +
    +Retry Template +The RetryTemplate is part of the Spring Retry library. +While it is out of scope of this document to cover all of the capabilities of the RetryTemplate, we will mention the following consumer properties that are specifically related to +the RetryTemplate: + + +maxAttempts + +The number of attempts to process the message. +Default: 3. + + + +backOffInitialInterval + +The backoff initial interval on retry. +Default 1000 milliseconds. + + + +backOffMaxInterval + +The maximum backoff interval. +Default 10000 milliseconds. + + + +backOffMultiplier + +The backoff multiplier. +Default 2.0. + + + +defaultRetryable + +Whether exceptions thrown by the listener that are not listed in the retryableExceptions are retryable. +Default: true. + + + +retryableExceptions + +A map of Throwable class names in the key and a boolean in the value. +Specify those exceptions (and subclasses) that will or won’t be retried. +Also see defaultRetriable. +Example: spring.cloud.stream.bindings.input.consumer.retryable-exceptions.java.lang.IllegalStateException=false. +Default: empty. + + + +While the preceding settings are sufficient for majority of the customization requirements, they may not satisfy certain complex requirements at, which +point you may want to provide your own instance of the RetryTemplate. To do so configure it as a bean in your application configuration. The application provided +instance will override the one provided by the framework. Also, to avoid conflicts you must qualify the instance of the RetryTemplate you want to be used by the binder +as @StreamRetryTemplate. For example, +@StreamRetryTemplate +public RetryTemplate myRetryTemplate() { + return new RetryTemplate(); +} +As you can see from the above example you don’t need to annotate it with @Bean since @StreamRetryTemplate is a qualified @Bean. +
    +
    +
    +Reactive Programming Support +Spring Cloud Stream also supports the use of reactive APIs where incoming and outgoing data is handled as continuous data flows. +Support for reactive APIs is available through spring-cloud-stream-reactive, which needs to be added explicitly to your project. +The programming model with reactive APIs is declarative. Instead of specifying how each individual message should be handled, you can use operators that describe functional transformations from inbound to outbound data flows. +At present Spring Cloud Stream supports the only the Reactor API. +In the future, we intend to support a more generic model based on Reactive Streams. +The reactive programming model also uses the @StreamListener annotation for setting up reactive handlers. +The differences are that: + + +The @StreamListener annotation must not specify an input or output, as they are provided as arguments and return values from the method. + + +The arguments of the method must be annotated with @Input and @Output, indicating which input or output the incoming and outgoing data flows connect to, respectively. + + +The return value of the method, if any, is annotated with @Output, indicating the input where data should be sent. + + + +Reactive programming support requires Java 1.8. + + +As of Spring Cloud Stream 1.1.1 and later (starting with release train Brooklyn.SR2), reactive programming support requires the use of Reactor 3.0.4.RELEASE and higher. +Earlier Reactor versions (including 3.0.1.RELEASE, 3.0.2.RELEASE and 3.0.3.RELEASE) are not supported. +spring-cloud-stream-reactive transitively retrieves the proper version, but it is possible for the project structure to manage the version of the io.projectreactor:reactor-core to an earlier release, especially when using Maven. +This is the case for projects generated by using Spring Initializr with Spring Boot 1.x, which overrides the Reactor version to 2.0.8.RELEASE. +In such cases, you must ensure that the proper version of the artifact is released. +You can do so by adding a direct dependency on io.projectreactor:reactor-core with a version of 3.0.4.RELEASE or later to your project. + + +The use of term, reactive, currently refers to the reactive APIs being used and not to the execution model being reactive (that is, the bound endpoints still use a 'push' rather than a 'pull' model). While some backpressure support is provided by the use of Reactor, we do intend, in a future release, to support entirely reactive pipelines by the use of native reactive clients for the connected middleware. + +
    +Reactor-based Handlers +A Reactor-based handler can have the following argument types: + + +For arguments annotated with @Input, it supports the Reactor Flux type. +The parameterization of the inbound Flux follows the same rules as in the case of individual message handling: It can be the entire Message, a POJO that can be the Message payload, or a POJO that is the result of a transformation based on the Message content-type header. Multiple inputs are provided. + + +For arguments annotated with Output, it supports the FluxSender type, which connects a Flux produced by the method with an output. Generally speaking, specifying outputs as arguments is only recommended when the method can have multiple outputs. + + +A Reactor-based handler supports a return type of Flux. In that case, it must be annotated with @Output. We recommend using the return value of the method when a single output Flux is available. +The following example shows a Reactor-based Processor: +@EnableBinding(Processor.class) +@EnableAutoConfiguration +public static class UppercaseTransformer { + + @StreamListener + @Output(Processor.OUTPUT) + public Flux<String> receive(@Input(Processor.INPUT) Flux<String> input) { + return input.map(s -> s.toUpperCase()); + } +} +The same processor using output arguments looks like the following example: +@EnableBinding(Processor.class) +@EnableAutoConfiguration +public static class UppercaseTransformer { + + @StreamListener + public void receive(@Input(Processor.INPUT) Flux<String> input, + @Output(Processor.OUTPUT) FluxSender output) { + output.send(input.map(s -> s.toUpperCase())); + } +} +
    +
    +Reactive Sources +Spring Cloud Stream reactive support also provides the ability for creating reactive sources through the @StreamEmitter annotation. +By using the @StreamEmitter annotation, a regular source may be converted to a reactive one. +@StreamEmitter is a method level annotation that marks a method to be an emitter to outputs declared with @EnableBinding. +You cannot use the @Input annotation along with @StreamEmitter, as the methods marked with this annotation are not listening for any input. Rather, methods marked with @StreamEmitter generate output. +Following the same programming model used in @StreamListener, @StreamEmitter also allows flexible ways of using the @Output annotation, depending on whether the method has any arguments, a return type, and other considerations. +The remainder of this section contains examples of using the @StreamEmitter annotation in various styles. +The following example emits the Hello, World message every millisecond and publishes to a Reactor Flux: +@EnableBinding(Source.class) +@EnableAutoConfiguration +public static class HelloWorldEmitter { + + @StreamEmitter + @Output(Source.OUTPUT) + public Flux<String> emit() { + return Flux.intervalMillis(1) + .map(l -> "Hello World"); + } +} +In the preceding example, the resulting messages in the Flux are sent to the output channel of the Source. +The next example is another flavor of an @StreamEmmitter that sends a Reactor Flux. +Instead of returning a Flux, the following method uses a FluxSender to programmatically send a Flux from a source: +@EnableBinding(Source.class) +@EnableAutoConfiguration +public static class HelloWorldEmitter { + + @StreamEmitter + @Output(Source.OUTPUT) + public void emit(FluxSender output) { + output.send(Flux.intervalMillis(1) + .map(l -> "Hello World")); + } +} +The next example is exactly same as the above snippet in functionality and style. +However, instead of using an explicit @Output annotation on the method, it uses the annotation on the method parameter. +@EnableBinding(Source.class) +@EnableAutoConfiguration +public static class HelloWorldEmitter { + + @StreamEmitter + public void emit(@Output(Source.OUTPUT) FluxSender output) { + output.send(Flux.intervalMillis(1) + .map(l -> "Hello World")); + } +} +The last example in this section is yet another flavor of writing reacting sources by using the Reactive Streams Publisher API and taking advantage of the support for it in Spring Integration Java DSL. +The Publisher in the following example still uses Reactor Flux under the hood, but, from an application perspective, that is transparent to the user and only needs Reactive Streams and Java DSL for Spring Integration: +@EnableBinding(Source.class) +@EnableAutoConfiguration +public static class HelloWorldEmitter { + + @StreamEmitter + @Output(Source.OUTPUT) + @Bean + public Publisher<Message<String>> emit() { + return IntegrationFlows.from(() -> + new GenericMessage<>("Hello World"), + e -> e.poller(p -> p.fixedDelay(1))) + .toReactivePublisher(); + } +} +
    +
    +
    + +Binders +Spring Cloud Stream provides a Binder abstraction for use in connecting to physical destinations at the external middleware. +This section provides information about the main concepts behind the Binder SPI, its main components, and implementation-specific details. +
    +Producers and Consumers +The following image shows the general relationship of producers and consumers: +
    +Producers and Consumers + + + + +producers consumers + +
    +A producer is any component that sends messages to a channel. +The channel can be bound to an external message broker with a Binder implementation for that broker. +When invoking the bindProducer() method, the first parameter is the name of the destination within the broker, the second parameter is the local channel instance to which the producer sends messages, and the third parameter contains properties (such as a partition key expression) to be used within the adapter that is created for that channel. +A consumer is any component that receives messages from a channel. +As with a producer, the consumer’s channel can be bound to an external message broker. +When invoking the bindConsumer() method, the first parameter is the destination name, and a second parameter provides the name of a logical group of consumers. +Each group that is represented by consumer bindings for a given destination receives a copy of each message that a producer sends to that destination (that is, it follows normal publish-subscribe semantics). +If there are multiple consumer instances bound with the same group name, then messages are load-balanced across those consumer instances so that each message sent by a producer is consumed by only a single consumer instance within each group (that is, it follows normal queueing semantics). +
    +
    +Binder SPI +The Binder SPI consists of a number of interfaces, out-of-the box utility classes, and discovery strategies that provide a pluggable mechanism for connecting to external middleware. +The key point of the SPI is the Binder interface, which is a strategy for connecting inputs and outputs to external middleware. The following listing shows the definnition of the Binder interface: +public interface Binder<T, C extends ConsumerProperties, P extends ProducerProperties> { + Binding<T> bindConsumer(String name, String group, T inboundBindTarget, C consumerProperties); + + Binding<T> bindProducer(String name, T outboundBindTarget, P producerProperties); +} +The interface is parameterized, offering a number of extension points: + + +Input and output bind targets. As of version 1.0, only MessageChannel is supported, but this is intended to be used as an extension point in the future. + + +Extended consumer and producer properties, allowing specific Binder implementations to add supplemental properties that can be supported in a type-safe manner. + + +A typical binder implementation consists of the following: + + +A class that implements the Binder interface; + + +A Spring @Configuration class that creates a bean of type Binder along with the middleware connection infrastructure. + + +A META-INF/spring.binders file found on the classpath containing one or more binder definitions, as shown in the following example: +kafka:\ +org.springframework.cloud.stream.binder.kafka.config.KafkaBinderConfiguration + + +
    +
    +Binder Detection +Spring Cloud Stream relies on implementations of the Binder SPI to perform the task of connecting channels to message brokers. +Each Binder implementation typically connects to one type of messaging system. +
    +Classpath Detection +By default, Spring Cloud Stream relies on Spring Boot’s auto-configuration to configure the binding process. +If a single Binder implementation is found on the classpath, Spring Cloud Stream automatically uses it. +For example, a Spring Cloud Stream project that aims to bind only to RabbitMQ can add the following dependency: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-binder-rabbit</artifactId> +</dependency> +For the specific Maven coordinates of other binder dependencies, see the documentation of that binder implementation. +
    +
    +
    +Multiple Binders on the Classpath +When multiple binders are present on the classpath, the application must indicate which binder is to be used for each channel binding. +Each binder configuration contains a META-INF/spring.binders file, which is a simple properties file, as shown in the following example: +rabbit:\ +org.springframework.cloud.stream.binder.rabbit.config.RabbitServiceAutoConfiguration +Similar files exist for the other provided binder implementations (such as Kafka), and custom binder implementations are expected to provide them as well. +The key represents an identifying name for the binder implementation, whereas the value is a comma-separated list of configuration classes that each contain one and only one bean definition of type org.springframework.cloud.stream.binder.Binder. +Binder selection can either be performed globally, using the spring.cloud.stream.defaultBinder property (for example, spring.cloud.stream.defaultBinder=rabbit) or individually, by configuring the binder on each channel binding. +For instance, a processor application (that has channels named input and output for read and write respectively) that reads from Kafka and writes to RabbitMQ can specify the following configuration: +spring.cloud.stream.bindings.input.binder=kafka +spring.cloud.stream.bindings.output.binder=rabbit +
    +
    +Connecting to Multiple Systems +By default, binders share the application’s Spring Boot auto-configuration, so that one instance of each binder found on the classpath is created. +If your application should connect to more than one broker of the same type, you can specify multiple binder configurations, each with different environment settings. + +Turning on explicit binder configuration disables the default binder configuration process altogether. +If you do so, all binders in use must be included in the configuration. +Frameworks that intend to use Spring Cloud Stream transparently may create binder configurations that can be referenced by name, but they do not affect the default binder configuration. +In order to do so, a binder configuration may have its defaultCandidate flag set to false (for example, spring.cloud.stream.binders.<configurationName>.defaultCandidate=false). +This denotes a configuration that exists independently of the default binder configuration process. + +The following example shows a typical configuration for a processor application that connects to two RabbitMQ broker instances: +spring: + cloud: + stream: + bindings: + input: + destination: thing1 + binder: rabbit1 + output: + destination: thing2 + binder: rabbit2 + binders: + rabbit1: + type: rabbit + environment: + spring: + rabbitmq: + host: <host1> + rabbit2: + type: rabbit + environment: + spring: + rabbitmq: + host: <host2> +
    +
    +Binding visualization and control +Since version 2.0, Spring Cloud Stream supports visualization and control of the Bindings through Actuator endpoints. +Starting with version 2.0 actuator and web are optional, you must first add one of the web dependencies as well as add the actuator dependency manually. +The following example shows how to add the dependency for the Web framework: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> +</dependency> +The following example shows how to add the dependency for the WebFlux framework: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> +</dependency> +You can add the Actuator dependency as follows: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> +</dependency> + +To run Spring Cloud Stream 2.0 apps in Cloud Foundry, you must add spring-boot-starter-web and spring-boot-starter-actuator to the classpath. Otherwise, the +application will not start due to health check failures. + +You must also enable the bindings actuator endpoints by setting the following property: --management.endpoints.web.exposure.include=bindings. +Once those prerequisites are satisfied. you should see the following in the logs when application start: +: Mapped "{[/actuator/bindings/{name}],methods=[POST]. . . +: Mapped "{[/actuator/bindings],methods=[GET]. . . +: Mapped "{[/actuator/bindings/{name}],methods=[GET]. . . +To visualize the current bindings, access the following URL: +http://<host>:<port>/actuator/bindings +Alternative, to see a single binding, access one of the URLs similar to the following: +http://<host>:<port>/actuator/bindings/myBindingName +You can also stop, start, pause, and resume individual bindings by posting to the same URL while providing a state argument as JSON, as shown in the following examples: +curl -d '{"state":"STOPPED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"STARTED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"PAUSED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName +curl -d '{"state":"RESUMED"}' -H "Content-Type: application/json" -X POST http://<host>:<port>/actuator/bindings/myBindingName + +PAUSED and RESUMED work only when the corresponding binder and its underlying technology supports it. Otherwise, you see the warning message in the logs. +Currently, only Kafka binder supports the PAUSED and RESUMED states. + +
    +
    +Binder Configuration Properties +The following properties are available when customizing binder configurations. These properties exposed via org.springframework.cloud.stream.config.BinderProperties +They must be prefixed with spring.cloud.stream.binders.<configurationName>. + + +type + +The binder type. +It typically references one of the binders found on the classpath — in particular, a key in a META-INF/spring.binders file. +By default, it has the same value as the configuration name. + + + +inheritEnvironment + +Whether the configuration inherits the environment of the application itself. +Default: true. + + + +environment + +Root for a set of properties that can be used to customize the environment of the binder. +When this property is set, the context in which the binder is being created is not a child of the application context. +This setting allows for complete separation between the binder components and the application components. +Default: empty. + + + +defaultCandidate + +Whether the binder configuration is a candidate for being considered a default binder or can be used only when explicitly referenced. +This setting allows adding binder configurations without interfering with the default processing. +Default: true. + + + +
    +
    + +Configuration Options +Spring Cloud Stream supports general configuration options as well as configuration for bindings and binders. +Some binders let additional binding properties support middleware-specific features. +Configuration options can be provided to Spring Cloud Stream applications through any mechanism supported by Spring Boot. +This includes application arguments, environment variables, and YAML or .properties files. +
    +Binding Service Properties +These properties are exposed via org.springframework.cloud.stream.config.BindingServiceProperties + + +spring.cloud.stream.instanceCount + +The number of deployed instances of an application. +Must be set for partitioning on the producer side. Must be set on the consumer side when using RabbitMQ and with Kafka if autoRebalanceEnabled=false. +Default: 1. + + + +spring.cloud.stream.instanceIndex + +The instance index of the application: A number from 0 to instanceCount - 1. +Used for partitioning with RabbitMQ and with Kafka if autoRebalanceEnabled=false. +Automatically set in Cloud Foundry to match the application’s instance index. + + + +spring.cloud.stream.dynamicDestinations + +A list of destinations that can be bound dynamically (for example, in a dynamic routing scenario). +If set, only listed destinations can be bound. +Default: empty (letting any destination be bound). + + + +spring.cloud.stream.defaultBinder + +The default binder to use, if multiple binders are configured. +See Multiple Binders on the Classpath. +Default: empty. + + + +spring.cloud.stream.overrideCloudConnectors + +This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. +If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). +When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). +The typical usage of this property is to be nested in a customized environment when connecting to multiple systems. +Default: false. + + + +spring.cloud.stream.bindingRetryInterval + +The interval (in seconds) between retrying binding creation when, for example, the binder does not support late binding and the broker (for example, Apache Kafka) is down. +Set it to zero to treat such conditions as fatal, preventing the application from starting. +Default: 30 + + + +
    +
    +Binding Properties +Binding properties are supplied by using the format of spring.cloud.stream.bindings.<channelName>.<property>=<value>. +The <channelName> represents the name of the channel being configured (for example, output for a Source). +To avoid repetition, Spring Cloud Stream supports setting values for all channels, in the format of spring.cloud.stream.default.<property>=<value>. +When it comes to avoiding repetitions for extended binding properties, this format should be used - spring.cloud.stream.<binder-type>.default.<producer|consumer>.<property>=<value>. +In what follows, we indicate where we have omitted the spring.cloud.stream.bindings.<channelName>. prefix and focus just on the property name, with the understanding that the prefix ise included at runtime. +
    +Common Binding Properties +These properties are exposed via org.springframework.cloud.stream.config.BindingProperties +The following binding properties are available for both input and output bindings and must be prefixed with spring.cloud.stream.bindings.<channelName>. (for example, spring.cloud.stream.bindings.input.destination=ticktock). +Default values can be set by using the spring.cloud.stream.default prefix (for example`spring.cloud.stream.default.contentType=application/json`). + + +destination + +The target destination of a channel on the bound middleware (for example, the RabbitMQ exchange or Kafka topic). +If the channel is bound as a consumer, it could be bound to multiple destinations, and the destination names can be specified as comma-separated String values. +If not set, the channel name is used instead. +The default value of this property cannot be overridden. + + + +group + +The consumer group of the channel. +Applies only to inbound bindings. +See Consumer Groups. +Default: null (indicating an anonymous consumer). + + + +contentType + +The content type of the channel. +See . +Default: application/json. + + + +binder + +The binder used by this binding. +See for details. +Default: null (the default binder is used, if it exists). + + + +
    +
    +Consumer Properties +These properties are exposed via org.springframework.cloud.stream.binder.ConsumerProperties +The following binding properties are available for input bindings only and must be prefixed with spring.cloud.stream.bindings.<channelName>.consumer. (for example, spring.cloud.stream.bindings.input.consumer.concurrency=3). +Default values can be set by using the spring.cloud.stream.default.consumer prefix (for example, spring.cloud.stream.default.consumer.headerMode=none). + + +concurrency + +The concurrency of the inbound consumer. +Default: 1. + + + +partitioned + +Whether the consumer receives data from a partitioned producer. +Default: false. + + + +headerMode + +When set to none, disables header parsing on input. +Effective only for messaging middleware that does not support message headers natively and requires header embedding. +This option is useful when consuming data from non-Spring Cloud Stream applications when native headers are not supported. +When set to headers, it uses the middleware’s native header mechanism. +When set to embeddedHeaders, it embeds headers into the message payload. +Default: depends on the binder implementation. + + + +maxAttempts + +If processing fails, the number of attempts to process the message (including the first). +Set to 1 to disable retry. +Default: 3. + + + +backOffInitialInterval + +The backoff initial interval on retry. +Default: 1000. + + + +backOffMaxInterval + +The maximum backoff interval. +Default: 10000. + + + +backOffMultiplier + +The backoff multiplier. +Default: 2.0. + + + +defaultRetryable + +Whether exceptions thrown by the listener that are not listed in the retryableExceptions are retryable. +Default: true. + + + +instanceIndex + +When set to a value greater than equal to zero, it allows customizing the instance index of this consumer (if different from spring.cloud.stream.instanceIndex). +When set to a negative value, it defaults to spring.cloud.stream.instanceIndex. +See for more information. +Default: -1. + + + +instanceCount + +When set to a value greater than equal to zero, it allows customizing the instance count of this consumer (if different from spring.cloud.stream.instanceCount). +When set to a negative value, it defaults to spring.cloud.stream.instanceCount. +See for more information. +Default: -1. + + + +retryableExceptions + +A map of Throwable class names in the key and a boolean in the value. +Specify those exceptions (and subclasses) that will or won’t be retried. +Also see defaultRetriable. +Example: spring.cloud.stream.bindings.input.consumer.retryable-exceptions.java.lang.IllegalStateException=false. +Default: empty. + + + +useNativeDecoding + +When set to true, the inbound message is deserialized directly by the client library, which must be configured correspondingly (for example, setting an appropriate Kafka producer value deserializer). +When this configuration is being used, the inbound message unmarshalling is not based on the contentType of the binding. +When native decoding is used, it is the responsibility of the producer to use an appropriate encoder (for example, the Kafka producer value serializer) to serialize the outbound message. +Also, when native encoding and decoding is used, the headerMode=embeddedHeaders property is ignored and headers are not embedded in the message. +See the producer property useNativeEncoding. +Default: false. + + + +
    +
    +Producer Properties +These properties are exposed via org.springframework.cloud.stream.binder.ProducerProperties +The following binding properties are available for output bindings only and must be prefixed with spring.cloud.stream.bindings.<channelName>.producer. (for example, spring.cloud.stream.bindings.input.producer.partitionKeyExpression=payload.id). +Default values can be set by using the prefix spring.cloud.stream.default.producer (for example, spring.cloud.stream.default.producer.partitionKeyExpression=payload.id). + + +partitionKeyExpression + +A SpEL expression that determines how to partition outbound data. +If set, or if partitionKeyExtractorClass is set, outbound data on this channel is partitioned. partitionCount must be set to a value greater than 1 to be effective. +Mutually exclusive with partitionKeyExtractorClass. +See . +Default: null. + + + +partitionKeyExtractorClass + +A PartitionKeyExtractorStrategy implementation. +If set, or if partitionKeyExpression is set, outbound data on this channel is partitioned. partitionCount must be set to a value greater than 1 to be effective. +Mutually exclusive with partitionKeyExpression. +See . +Default: null. + + + +partitionSelectorClass + + A PartitionSelectorStrategy implementation. +Mutually exclusive with partitionSelectorExpression. +If neither is set, the partition is selected as the hashCode(key) % partitionCount, where key is computed through either partitionKeyExpression or partitionKeyExtractorClass. +Default: null. + + + +partitionSelectorExpression + +A SpEL expression for customizing partition selection. +Mutually exclusive with partitionSelectorClass. +If neither is set, the partition is selected as the hashCode(key) % partitionCount, where key is computed through either partitionKeyExpression or partitionKeyExtractorClass. +Default: null. + + + +partitionCount + +The number of target partitions for the data, if partitioning is enabled. +Must be set to a value greater than 1 if the producer is partitioned. +On Kafka, it is interpreted as a hint. The larger of this and the partition count of the target topic is used instead. +Default: 1. + + + +requiredGroups + +A comma-separated list of groups to which the producer must ensure message delivery even if they start after it has been created (for example, by pre-creating durable queues in RabbitMQ). + + + +headerMode + +When set to none, it disables header embedding on output. +It is effective only for messaging middleware that does not support message headers natively and requires header embedding. +This option is useful when producing data for non-Spring Cloud Stream applications when native headers are not supported. +When set to headers, it uses the middleware’s native header mechanism. +When set to embeddedHeaders, it embeds headers into the message payload. +Default: Depends on the binder implementation. + + + +useNativeEncoding + +When set to true, the outbound message is serialized directly by the client library, which must be configured correspondingly (for example, setting an appropriate Kafka producer value serializer). +When this configuration is being used, the outbound message marshalling is not based on the contentType of the binding. +When native encoding is used, it is the responsibility of the consumer to use an appropriate decoder (for example, the Kafka consumer value de-serializer) to deserialize the inbound message. +Also, when native encoding and decoding is used, the headerMode=embeddedHeaders property is ignored and headers are not embedded in the message. +See the consumer property useNativeDecoding. +Default: false. + + + +errorChannelEnabled + +When set to true, if the binder supports asynchroous send results, send failures are sent to an error channel for the destination. +See for more information. +Default: false. + + + +
    +
    +
    +Using Dynamically Bound Destinations +Besides the channels defined by using @EnableBinding, Spring Cloud Stream lets applications send messages to dynamically bound destinations. +This is useful, for example, when the target destination needs to be determined at runtime. +Applications can do so by using the BinderAwareChannelResolver bean, registered automatically by the @EnableBinding annotation. +The 'spring.cloud.stream.dynamicDestinations' property can be used for restricting the dynamic destination names to a known set (whitelisting). +If this property is not set, any destination can be bound dynamically. +The BinderAwareChannelResolver can be used directly, as shown in the following example of a REST controller using a path variable to decide the target channel: +@EnableBinding +@Controller +public class SourceWithDynamicDestination { + + @Autowired + private BinderAwareChannelResolver resolver; + + @RequestMapping(path = "/{target}", method = POST, consumes = "*/*") + @ResponseStatus(HttpStatus.ACCEPTED) + public void handleRequest(@RequestBody String body, @PathVariable("target") target, + @RequestHeader(HttpHeaders.CONTENT_TYPE) Object contentType) { + sendMessage(body, target, contentType); + } + + private void sendMessage(String body, String target, Object contentType) { + resolver.resolveDestination(target).send(MessageBuilder.createMessage(body, + new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType)))); + } +} +Now consider what happens when we start the application on the default port (8080) and make the following requests with CURL: +curl -H "Content-Type: application/json" -X POST -d "customer-1" http://localhost:8080/customers + +curl -H "Content-Type: application/json" -X POST -d "order-1" http://localhost:8080/orders +The destinations, 'customers' and 'orders', are created in the broker (in the exchange for Rabbit or in the topic for Kafka) with names of 'customers' and 'orders', and the data is published to the appropriate destinations. +The BinderAwareChannelResolver is a general-purpose Spring Integration DestinationResolver and can be injected in other components — for example, in a router using a SpEL expression based on the target field of an incoming JSON message. The following example includes a router that reads SpEL expressions: +@EnableBinding +@Controller +public class SourceWithDynamicDestination { + + @Autowired + private BinderAwareChannelResolver resolver; + + + @RequestMapping(path = "/", method = POST, consumes = "application/json") + @ResponseStatus(HttpStatus.ACCEPTED) + public void handleRequest(@RequestBody String body, @RequestHeader(HttpHeaders.CONTENT_TYPE) Object contentType) { + sendMessage(body, contentType); + } + + private void sendMessage(Object body, Object contentType) { + routerChannel().send(MessageBuilder.createMessage(body, + new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType)))); + } + + @Bean(name = "routerChannel") + public MessageChannel routerChannel() { + return new DirectChannel(); + } + + @Bean + @ServiceActivator(inputChannel = "routerChannel") + public ExpressionEvaluatingRouter router() { + ExpressionEvaluatingRouter router = + new ExpressionEvaluatingRouter(new SpelExpressionParser().parseExpression("payload.target")); + router.setDefaultOutputChannelName("default-output"); + router.setChannelResolver(resolver); + return router; + } +} +The Router Sink Application uses this technique to create the destinations on-demand. +If the channel names are known in advance, you can configure the producer properties as with any other destination. +Alternatively, if you register a NewBindingCallback<> bean, it is invoked just before the binding is created. +The callback takes the generic type of the extended producer properties used by the binder. +It has one method: +void configure(String channelName, MessageChannel channel, ProducerProperties producerProperties, + T extendedProducerProperties); +The following example shows how to use the RabbitMQ binder: +@Bean +public NewBindingCallback<RabbitProducerProperties> dynamicConfigurer() { + return (name, channel, props, extended) -> { + props.setRequiredGroups("bindThisQueue"); + extended.setQueueNameGroupOnly(true); + extended.setAutoBindDlq(true); + extended.setDeadLetterQueueName("myDLQ"); + }; +} + +If you need to support dynamic destinations with multiple binder types, use Object for the generic type and cast the extended argument as needed. + +
    +
    + +Content Type Negotiation +Data transformation is one of the core features of any message-driven microservice architecture. Given that, in Spring Cloud Stream, such data +is represented as a Spring Message, a message may have to be transformed to a desired shape or size before reaching its destination. This is required for two reasons: + + +To convert the contents of the incoming message to match the signature of the application-provided handler. + + +To convert the contents of the outgoing message to the wire format. + + +The wire format is typically byte[] (that is true for the Kafka and Rabbit binders), but it is governed by the binder implementation. +In Spring Cloud Stream, message transformation is accomplished with an org.springframework.messaging.converter.MessageConverter. + +As a supplement to the details to follow, you may also want to read the following blog post. + +
    +Mechanics +To better understand the mechanics and the necessity behind content-type negotiation, we take a look at a very simple use case by using the following message handler as an example: +@StreamListener(Processor.INPUT) +@SendTo(Processor.OUTPUT) +public String handle(Person person) {..} + +For simplicity, we assume that this is the only handler in the application (we assume there is no internal pipeline). + +The handler shown in the preceding example expects a Person object as an argument and produces a String type as an output. +In order for the framework to succeed in passing the incoming Message as an argument to this handler, it has to somehow transform the payload of the Message type from the wire format to a Person type. +In other words, the framework must locate and apply the appropriate MessageConverter. +To accomplish that, the framework needs some instructions from the user. +One of these instructions is already provided by the signature of the handler method itself (Person type). +Consequently, in theory, that should be (and, in some cases, is) enough. +However, for the majority of use cases, in order to select the appropriate MessageConverter, the framework needs an additional piece of information. +That missing piece is contentType. +Spring Cloud Stream provides three mechanisms to define contentType (in order of precedence): + + +HEADER: The contentType can be communicated through the Message itself. By providing a contentType header, you declare the content type to use to locate and apply the appropriate MessageConverter. + + +BINDING: The contentType can be set per destination binding by setting the spring.cloud.stream.bindings.input.content-type property. + +The input segment in the property name corresponds to the actual name of the destination (which is “input” in our case). This approach lets you declare, on a per-binding basis, the content type to use to locate and apply the appropriate MessageConverter. + + + +DEFAULT: If contentType is not present in the Message header or the binding, the default application/json content type is used to +locate and apply the appropriate MessageConverter. + + +As mentioned earlier, the preceding list also demonstrates the order of precedence in case of a tie. For example, a header-provided content type takes precedence over any other content type. +The same applies for a content type set on a per-binding basis, which essentially lets you override the default content type. +However, it also provides a sensible default (which was determined from community feedback). +Another reason for making application/json the default stems from the interoperability requirements driven by distributed microservices architectures, where producer and consumer not only run in different JVMs but can also run on different non-JVM platforms. +When the non-void handler method returns, if the the return value is already a Message, that Message becomes the payload. However, when the return value is not a Message, the new Message is constructed with the return value as the payload while inheriting +headers from the input Message minus the headers defined or filtered by SpringIntegrationProperties.messageHandlerNotPropagatedHeaders. +By default, there is only one header set there: contentType. This means that the new Message does not have contentType header set, thus ensuring that the contentType can evolve. +You can always opt out of returning a Message from the handler method where you can inject any header you wish. +If there is an internal pipeline, the Message is sent to the next handler by going through the same process of conversion. However, if there is no internal pipeline or you have reached the end of it, the Message is sent back to the output destination. +
    +Content Type versus Argument Type +As mentioned earlier, for the framework to select the appropriate MessageConverter, it requires argument type and, optionally, content type information. +The logic for selecting the appropriate MessageConverter resides with the argument resolvers (HandlerMethodArgumentResolvers), which trigger right before the invocation of the user-defined handler method (which is when the actual argument type is known to the framework). +If the argument type does not match the type of the current payload, the framework delegates to the stack of the +pre-configured MessageConverters to see if any one of them can convert the payload. +As you can see, the Object fromMessage(Message<?> message, Class<?> targetClass); +operation of the MessageConverter takes targetClass as one of its arguments. +The framework also ensures that the provided Message always contains a contentType header. +When no contentType header was already present, it injects either the per-binding contentType header or the default contentType header. +The combination of contentType argument type is the mechanism by which framework determines if message can be converted to a target type. +If no appropriate MessageConverter is found, an exception is thrown, which you can handle by adding a custom MessageConverter (see ). +But what if the payload type matches the target type declared by the handler method? In this case, there is nothing to convert, and the +payload is passed unmodified. While this sounds pretty straightforward and logical, keep in mind handler methods that take a Message<?> or Object as an argument. +By declaring the target type to be Object (which is an instanceof everything in Java), you essentially forfeit the conversion process. + +Do not expect Message to be converted into some other type based only on the contentType. +Remember that the contentType is complementary to the target type. +If you wish, you can provide a hint, which MessageConverter may or may not take into consideration. + +
    +
    +Message Converters +MessageConverters define two methods: +Object fromMessage(Message<?> message, Class<?> targetClass); + +Message<?> toMessage(Object payload, @Nullable MessageHeaders headers); +It is important to understand the contract of these methods and their usage, specifically in the context of Spring Cloud Stream. +The fromMessage method converts an incoming Message to an argument type. +The payload of the Message could be any type, and it is +up to the actual implementation of the MessageConverter to support multiple types. +For example, some JSON converter may support the payload type as byte[], String, and others. +This is important when the application contains an internal pipeline (that is, input → handler1 → handler2 →. . . → output) and the output of the upstream handler results in a Message which may not be in the initial wire format. +However, the toMessage method has a more strict contract and must always convert Message to the wire format: byte[]. +So, for all intents and purposes (and especially when implementing your own converter) you regard the two methods as having the following signatures: +Object fromMessage(Message<?> message, Class<?> targetClass); + +Message<byte[]> toMessage(Object payload, @Nullable MessageHeaders headers); +
    +
    +
    +Provided MessageConverters +As mentioned earlier, the framework already provides a stack of MessageConverters to handle most common use cases. +The following list describes the provided MessageConverters, in order of precedence (the first MessageConverter that works is used): + + +ApplicationJsonMessageMarshallingConverter: Variation of the org.springframework.messaging.converter.MappingJackson2MessageConverter. Supports conversion of the payload of the Message to/from POJO for cases when contentType is application/json (DEFAULT). + + +TupleJsonMessageConverter: DEPRECATED Supports conversion of the payload of the Message to/from org.springframework.tuple.Tuple. + + +ByteArrayMessageConverter: Supports conversion of the payload of the Message from byte[] to byte[] for cases when contentType is application/octet-stream. It is essentially a pass through and exists primarily for backward compatibility. + + +ObjectStringMessageConverter: Supports conversion of any type to a String when contentType is text/plain. +It invokes Object’s toString() method or, if the payload is byte[], a new String(byte[]). + + +JavaSerializationMessageConverter: DEPRECATED Supports conversion based on java serialization when contentType is application/x-java-serialized-object. + + +KryoMessageConverter: DEPRECATED Supports conversion based on Kryo serialization when contentType is application/x-java-object. + + +JsonUnmarshallingConverter: Similar to the ApplicationJsonMessageMarshallingConverter. It supports conversion of any type when contentType is application/x-java-object. +It expects the actual type information to be embedded in the contentType as an attribute (for example, application/x-java-object;type=foo.bar.Cat). + + +When no appropriate converter is found, the framework throws an exception. When that happens, you should check your code and configuration and ensure you did not miss anything (that is, ensure that you provided a contentType by using a binding or a header). +However, most likely, you found some uncommon case (such as a custom contentType perhaps) and the current stack of provided MessageConverters +does not know how to convert. If that is the case, you can add custom MessageConverter. See . +
    +
    +User-defined Message Converters +Spring Cloud Stream exposes a mechanism to define and register additional MessageConverters. +To use it, implement org.springframework.messaging.converter.MessageConverter, configure it as a @Bean, and annotate it with @StreamMessageConverter. +It is then apended to the existing stack of `MessageConverter`s. + +It is important to understand that custom MessageConverter implementations are added to the head of the existing stack. +Consequently, custom MessageConverter implementations take precedence over the existing ones, which lets you override as well as add to the existing converters. + +The following example shows how to create a message converter bean to support a new content type called application/bar: +@EnableBinding(Sink.class) +@SpringBootApplication +public static class SinkApplication { + + ... + + @Bean + @StreamMessageConverter + public MessageConverter customMessageConverter() { + return new MyCustomMessageConverter(); + } +} + +public class MyCustomMessageConverter extends AbstractMessageConverter { + + public MyCustomMessageConverter() { + super(new MimeType("application", "bar")); + } + + @Override + protected boolean supports(Class<?> clazz) { + return (Bar.class.equals(clazz)); + } + + @Override + protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) { + Object payload = message.getPayload(); + return (payload instanceof Bar ? payload : new Bar((byte[]) payload)); + } +} +Spring Cloud Stream also provides support for Avro-based converters and schema evolution. +See for details. +
    +
    + +Schema Evolution Support +Spring Cloud Stream provides support for schema evolution so that the data can be evolved over time and still work with older or newer producers and consumers and vice versa. +Most serialization models, especially the ones that aim for portability across different platforms and languages, rely on a schema that describes how the data is serialized in the binary payload. +In order to serialize the data and then to interpret it, both the sending and receiving sides must have access to a schema that describes the binary format. +In certain cases, the schema can be inferred from the payload type on serialization or from the target type on deserialization. +However, many applications benefit from having access to an explicit schema that describes the binary data format. +A schema registry lets you store schema information in a textual format (typically JSON) and makes that information accessible to various applications that need it to receive and send data in binary format. +A schema is referenceable as a tuple consisting of: + + +A subject that is the logical name of the schema + + +The schema version + + +The schema format, which describes the binary format of the data + + +This following sections goes through the details of various components involved in schema evolution process. +
    +Schema Registry Client +The client-side abstraction for interacting with schema registry servers is the SchemaRegistryClient interface, which has the following structure: +public interface SchemaRegistryClient { + + SchemaRegistrationResponse register(String subject, String format, String schema); + + String fetch(SchemaReference schemaReference); + + String fetch(Integer id); + +} +Spring Cloud Stream provides out-of-the-box implementations for interacting with its own schema server and for interacting with the Confluent Schema Registry. +A client for the Spring Cloud Stream schema registry can be configured by using the @EnableSchemaRegistryClient, as follows: + @EnableBinding(Sink.class) + @SpringBootApplication + @EnableSchemaRegistryClient + public static class AvroSinkApplication { + ... + } + +The default converter is optimized to cache not only the schemas from the remote server but also the parse() and toString() methods, which are quite expensive. +Because of this, it uses a DefaultSchemaRegistryClient that does not cache responses. +If you intend to change the default behavior, you can use the client directly on your code and override it to the desired outcome. +To do so, you have to add the property spring.cloud.stream.schemaRegistryClient.cached=true to your application properties. + +
    +Schema Registry Client Properties +The Schema Registry Client supports the following properties: + + +spring.cloud.stream.schemaRegistryClient.endpoint + +The location of the schema-server. +When setting this, use a full URL, including protocol (http or https) , port, and context path. + + + +Default + +http://localhost:8990/ + + + +spring.cloud.stream.schemaRegistryClient.cached + +Whether the client should cache schema server responses. +Normally set to false, as the caching happens in the message converter. +Clients using the schema registry client should set this to true. + + + +Default + +true + + + +
    +
    +
    +Avro Schema Registry Client Message Converters +For applications that have a SchemaRegistryClient bean registered with the application context, Spring Cloud Stream auto configures an Apache Avro message converter for schema management. +This eases schema evolution, as applications that receive messages can get easy access to a writer schema that can be reconciled with their own reader schema. +For outbound messages, if the content type of the channel is set to application/*+avro, the MessageConverter is activated, as shown in the following example: +spring.cloud.stream.bindings.output.contentType=application/*+avro +During the outbound conversion, the message converter tries to infer the schema of each outbound messages (based on its type) and register it to a subject (based on the payload type) by using the SchemaRegistryClient. +If an identical schema is already found, then a reference to it is retrieved. +If not, the schema is registered, and a new version number is provided. +The message is sent with a contentType header by using the following scheme: application/[prefix].[subject].v[version]+avro, where prefix is configurable and subject is deduced from the payload type. +For example, a message of the type User might be sent as a binary payload with a content type of application/vnd.user.v2+avro, where user is the subject and 2 is the version number. +When receiving messages, the converter infers the schema reference from the header of the incoming message and tries to retrieve it. The schema is used as the writer schema in the deserialization process. +
    +Avro Schema Registry Message Converter Properties +If you have enabled Avro based schema registry client by setting spring.cloud.stream.bindings.output.contentType=application/*+avro, you can customize the behavior of the registration by setting the following properties. + + +spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled + +Enable if you want the converter to use reflection to infer a Schema from a POJO. +Default: false + + + +spring.cloud.stream.schema.avro.readerSchema + +Avro compares schema versions by looking at a writer schema (origin payload) and a reader schema (your application payload). See the Avro documentation for more information. If set, this overrides any lookups at the schema server and uses the local schema as the reader schema. +Default: null + + + +spring.cloud.stream.schema.avro.schemaLocations + +Registers any .avsc files listed in this property with the Schema Server. +Default: empty + + + +spring.cloud.stream.schema.avro.prefix + +The prefix to be used on the Content-Type header. +Default: vnd + + + +
    +
    +
    +Apache Avro Message Converters +Spring Cloud Stream provides support for schema-based message converters through its spring-cloud-stream-schema module. +Currently, the only serialization format supported out of the box for schema-based message converters is Apache Avro, with more formats to be added in future versions. +The spring-cloud-stream-schema module contains two types of message converters that can be used for Apache Avro serialization: + + +Converters that use the class information of the serialized or deserialized objects or a schema with a location known at startup. + + +Converters that use a schema registry. They locate the schemas at runtime and dynamically register new schemas as domain objects evolve. + + +
    +
    +Converters with Schema Support +The AvroSchemaMessageConverter supports serializing and deserializing messages either by using a predefined schema or by using the schema information available in the class (either reflectively or contained in the SpecificRecord). +If you provide a custom converter, then the default AvroSchemaMessageConverter bean is not created. The following example shows a custom converter: +To use custom converters, you can simply add it to the application context, optionally specifying one or more MimeTypes with which to associate it. +The default MimeType is application/avro. +If the target type of the conversion is a GenericRecord, a schema must be set. +The following example shows how to configure a converter in a sink application by registering the Apache Avro MessageConverter without a predefined schema. +In this example, note that the mime type value is avro/bytes, not the default application/avro. +@EnableBinding(Sink.class) +@SpringBootApplication +public static class SinkApplication { + + ... + + @Bean + public MessageConverter userMessageConverter() { + return new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes")); + } +} +Conversely, the following application registers a converter with a predefined schema (found on the classpath): +@EnableBinding(Sink.class) +@SpringBootApplication +public static class SinkApplication { + + ... + + @Bean + public MessageConverter userMessageConverter() { + AvroSchemaMessageConverter converter = new AvroSchemaMessageConverter(MimeType.valueOf("avro/bytes")); + converter.setSchemaLocation(new ClassPathResource("schemas/User.avro")); + return converter; + } +} +
    +
    +Schema Registry Server +Spring Cloud Stream provides a schema registry server implementation. +To use it, you can add the spring-cloud-stream-schema-server artifact to your project and use the @EnableSchemaRegistryServer annotation, which adds the schema registry server REST controller to your application. +This annotation is intended to be used with Spring Boot web applications, and the listening port of the server is controlled by the server.port property. +The spring.cloud.stream.schema.server.path property can be used to control the root path of the schema server (especially when it is embedded in other applications). +The spring.cloud.stream.schema.server.allowSchemaDeletion boolean property enables the deletion of a schema. By default, this is disabled. +The schema registry server uses a relational database to store the schemas. +By default, it uses an embedded database. +You can customize the schema storage by using the Spring Boot SQL database and JDBC configuration options. +The following example shows a Spring Boot application that enables the schema registry: +@SpringBootApplication +@EnableSchemaRegistryServer +public class SchemaRegistryServerApplication { + public static void main(String[] args) { + SpringApplication.run(SchemaRegistryServerApplication.class, args); + } +} +
    +Schema Registry Server API +The Schema Registry Server API consists of the following operations: + + +POST / — see + + +'GET /{subject}/{format}/{version}' — see + + +GET /{subject}/{format} — see + + +GET /schemas/{id} — see + + +DELETE /{subject}/{format}/{version} — see + + +DELETE /schemas/{id} — see + + +DELETE /{subject} — see + + +
    +Registering a New Schema +To register a new schema, send a POST request to the / endpoint. +The / accepts a JSON payload with the following fields: + + +subject: The schema subject + + +format: The schema format + + +definition: The schema definition + + +Its response is a schema object in JSON, with the following fields: + + +id: The schema ID + + +subject: The schema subject + + +format: The schema format + + +version: The schema version + + +definition: The schema definition + + +
    +
    +Retrieving an Existing Schema by Subject, Format, and Version +To retrieve an existing schema by subject, format, and version, send GET request to the /{subject}/{format}/{version} endpoint. +Its response is a schema object in JSON, with the following fields: + + +id: The schema ID + + +subject: The schema subject + + +format: The schema format + + +version: The schema version + + +definition: The schema definition + + +
    +
    +Retrieving an Existing Schema by Subject and Format +To retrieve an existing schema by subject and format, send a GET request to the /subject/format endpoint. +Its response is a list of schemas with each schema object in JSON, with the following fields: + + +id: The schema ID + + +subject: The schema subject + + +format: The schema format + + +version: The schema version + + +definition: The schema definition + + +
    +
    +Retrieving an Existing Schema by ID +To retrieve a schema by its ID, send a GET request to the /schemas/{id} endpoint. +Its response is a schema object in JSON, with the following fields: + + +id: The schema ID + + +subject: The schema subject + + +format: The schema format + + +version: The schema version + + +definition: The schema definition + + +
    +
    +Deleting a Schema by Subject, Format, and Version +To delete a schema identified by its subject, format, and version, send a DELETE request to the /{subject}/{format}/{version} endpoint. +
    +
    +Deleting a Schema by ID +To delete a schema by its ID, send a DELETE request to the /schemas/{id} endpoint. +
    +
    +Deleting a Schema by Subject +DELETE /{subject} +Delete existing schemas by their subject. + +This note applies to users of Spring Cloud Stream 1.1.0.RELEASE only. +Spring Cloud Stream 1.1.0.RELEASE used the table name, schema, for storing Schema objects. Schema is a keyword in a number of database implementations. +To avoid any conflicts in the future, starting with 1.1.1.RELEASE, we have opted for the name SCHEMA_REPOSITORY for the storage table. +Any Spring Cloud Stream 1.1.0.RELEASE users who upgrade should migrate their existing schemas to the new table before upgrading. + +
    +
    +
    +Using Confluent’s Schema Registry +The default configuration creates a DefaultSchemaRegistryClient bean. +If you want to use the Confluent schema registry, you need to create a bean of type ConfluentSchemaRegistryClient, which supersedes the one configured by default by the framework. The following example shows how to create such a bean: +@Bean +public SchemaRegistryClient schemaRegistryClient(@Value("${spring.cloud.stream.schemaRegistryClient.endpoint}") String endpoint){ + ConfluentSchemaRegistryClient client = new ConfluentSchemaRegistryClient(); + client.setEndpoint(endpoint); + return client; +} + +The ConfluentSchemaRegistryClient is tested against Confluent platform version 4.0.0. + +
    +
    +
    +Schema Registration and Resolution +To better understand how Spring Cloud Stream registers and resolves new schemas and its use of Avro schema comparison features, we provide two separate subsections: + + + + + + + + +
    +Schema Registration Process (Serialization) +The first part of the registration process is extracting a schema from the payload that is being sent over a channel. +Avro types such as SpecificRecord or GenericRecord already contain a schema, which can be retrieved immediately from the instance. +In the case of POJOs, a schema is inferred if the spring.cloud.stream.schema.avro.dynamicSchemaGenerationEnabled property is set to true (the default). +
    +Schema Writer Resolution Process + + + + +schema resolution + +
    +Ones a schema is obtained, the converter loads its metadata (version) from the remote server. +First, it queries a local cache. If no result is found, it submits the data to the server, which replies with versioning information. +The converter always caches the results to avoid the overhead of querying the Schema Server for every new message that needs to be serialized. +
    +Schema Registration Process + + + + +registration + +
    +With the schema version information, the converter sets the contentType header of the message to carry the version information — for example: application/vnd.user.v1+avro. +
    +
    +Schema Resolution Process (Deserialization) +When reading messages that contain version information (that is, a contentType header with a scheme like the one described under ), the converter queries the Schema server to fetch the writer schema of the message. +Once it has found the correct schema of the incoming message, it retrieves the reader schema and, by using Avro’s schema resolution support, reads it into the reader definition (setting defaults and any missing properties). +
    +Schema Reading Resolution Process + + + + +schema reading + +
    + +You should understand the difference between a writer schema (the application that wrote the message) and a reader schema (the receiving application). +We suggest taking a moment to read the Avro terminology and understand the process. +Spring Cloud Stream always fetches the writer schema to determine how to read a message. +If you want to get Avro’s schema evolution support working, you need to make sure that a readerSchema was properly set for your application. + +
    +
    +
    + +Inter-Application Communication +Spring Cloud Stream enables communication between applications. Inter-application communication is a complex issue spanning several concerns, as described in the following topics: + + + + + + + + + + + +
    +Connecting Multiple Application Instances +While Spring Cloud Stream makes it easy for individual Spring Boot applications to connect to messaging systems, the typical scenario for Spring Cloud Stream is the creation of multi-application pipelines, where microservice applications send data to each other. +You can achieve this scenario by correlating the input and output destinations of adjacent applications. +Suppose a design calls for the Time Source application to send data to the Log Sink application. You could use a common destination named ticktock for bindings within both applications. +Time Source (that has the channel name output) would set the following property: +spring.cloud.stream.bindings.output.destination=ticktock +Log Sink (that has the channel name input) would set the following property: +spring.cloud.stream.bindings.input.destination=ticktock +
    +
    +Instance Index and Instance Count +When scaling up Spring Cloud Stream applications, each instance can receive information about how many other instances of the same application exist and what its own instance index is. +Spring Cloud Stream does this through the spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex properties. +For example, if there are three instances of a HDFS sink application, all three instances have spring.cloud.stream.instanceCount set to 3, and the individual applications have spring.cloud.stream.instanceIndex set to 0, 1, and 2, respectively. +When Spring Cloud Stream applications are deployed through Spring Cloud Data Flow, these properties are configured automatically; when Spring Cloud Stream applications are launched independently, these properties must be set correctly. +By default, spring.cloud.stream.instanceCount is 1, and spring.cloud.stream.instanceIndex is 0. +In a scaled-up scenario, correct configuration of these two properties is important for addressing partitioning behavior (see below) in general, and the two properties are always required by certain binders (for example, the Kafka binder) in order to ensure that data are split correctly across multiple consumer instances. +
    +
    +Partitioning +Partitioning in Spring Cloud Stream consists of two tasks: + + + + + + + + +
    +Configuring Output Bindings for Partitioning +You can configure an output binding to send partitioned data by setting one and only one of its partitionKeyExpression or partitionKeyExtractorName properties, as well as its partitionCount property. +For example, the following is a valid and typical configuration: +spring.cloud.stream.bindings.output.producer.partitionKeyExpression=payload.id +spring.cloud.stream.bindings.output.producer.partitionCount=5 +Based on that example configuration, data is sent to the target partition by using the following logic. +A partition key’s value is calculated for each message sent to a partitioned output channel based on the partitionKeyExpression. +The partitionKeyExpression is a SpEL expression that is evaluated against the outbound message for extracting the partitioning key. +If a SpEL expression is not sufficient for your needs, you can instead calculate the partition key value by providing an implementation of org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy and configuring it as a bean (by using the @Bean annotation). +If you have more then one bean of type org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy available in the Application Context, you can further filter it by specifying its name with the partitionKeyExtractorName property, as shown in the following example: +--spring.cloud.stream.bindings.output.producer.partitionKeyExtractorName=customPartitionKeyExtractor +--spring.cloud.stream.bindings.output.producer.partitionCount=5 +. . . +@Bean +public CustomPartitionKeyExtractorClass customPartitionKeyExtractor() { + return new CustomPartitionKeyExtractorClass(); +} + +In previous versions of Spring Cloud Stream, you could specify the implementation of org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy by setting the spring.cloud.stream.bindings.output.producer.partitionKeyExtractorClass property. +Since version 2.0, this property is deprecated, and support for it will be removed in a future version. + +Once the message key is calculated, the partition selection process determines the target partition as a value between 0 and partitionCount - 1. +The default calculation, applicable in most scenarios, is based on the following formula: key.hashCode() % partitionCount. +This can be customized on the binding, either by setting a SpEL expression to be evaluated against the 'key' (through the partitionSelectorExpression property) or by configuring an implementation of org.springframework.cloud.stream.binder.PartitionSelectorStrategy as a bean (by using the @Bean annotation). +Similar to the PartitionKeyExtractorStrategy, you can further filter it by using the spring.cloud.stream.bindings.output.producer.partitionSelectorName property when more than one bean of this type is available in the Application Context, as shown in the following example: +--spring.cloud.stream.bindings.output.producer.partitionSelectorName=customPartitionSelector +. . . +@Bean +public CustomPartitionSelectorClass customPartitionSelector() { + return new CustomPartitionSelectorClass(); +} + +In previous versions of Spring Cloud Stream you could specify the implementation of org.springframework.cloud.stream.binder.PartitionSelectorStrategy by setting the spring.cloud.stream.bindings.output.producer.partitionSelectorClass property. +Since version 2.0, this property is deprecated and support for it will be removed in a future version. + +
    +
    +Configuring Input Bindings for Partitioning +An input binding (with the channel name input) is configured to receive partitioned data by setting its partitioned property, as well as the instanceIndex and instanceCount properties on the application itself, as shown in the following example: +spring.cloud.stream.bindings.input.consumer.partitioned=true +spring.cloud.stream.instanceIndex=3 +spring.cloud.stream.instanceCount=5 +The instanceCount value represents the total number of application instances between which the data should be partitioned. +The instanceIndex must be a unique value across the multiple instances, with a value between 0 and instanceCount - 1. +The instance index helps each application instance to identify the unique partition(s) from which it receives data. +It is required by binders using technology that does not support partitioning natively. +For example, with RabbitMQ, there is a queue for each partition, with the queue name containing the instance index. +With Kafka, if autoRebalanceEnabled is true (default), Kafka takes care of distributing partitions across instances, and these properties are not required. +If autoRebalanceEnabled is set to false, the instanceCount and instanceIndex are used by the binder to determine which partition(s) the instance subscribes to (you must have at least as many partitions as there are instances). +The binder allocates the partitions instead of Kafka. +This might be useful if you want messages for a particular partition to always go to the same instance. +When a binder configuration requires them, it is important to set both values correctly in order to ensure that all of the data is consumed and that the application instances receive mutually exclusive datasets. +While a scenario in which using multiple instances for partitioned data processing may be complex to set up in a standalone case, Spring Cloud Dataflow can simplify the process significantly by populating both the input and output values correctly and by letting you rely on the runtime infrastructure to provide information about the instance index and instance count. +
    +
    +
    + +Testing +Spring Cloud Stream provides support for testing your microservice applications without connecting to a messaging system. +You can do that by using the TestSupportBinder provided by the spring-cloud-stream-test-support library, which can be added as a test dependency to the application, as shown in the following example: + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-test-support</artifactId> + <scope>test</scope> + </dependency> + +The TestSupportBinder uses the Spring Boot autoconfiguration mechanism to supersede the other binders found on the classpath. +Therefore, when adding a binder as a dependency, you must make sure that the test scope is being used. + +The TestSupportBinder lets you interact with the bound channels and inspect any messages sent and received by the application. +For outbound message channels, the TestSupportBinder registers a single subscriber and retains the messages emitted by the application in a MessageCollector. +They can be retrieved during tests and have assertions made against them. +You can also send messages to inbound message channels so that the consumer application can consume the messages. +The following example shows how to test both input and output channels on a processor: +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ExampleTest { + + @Autowired + private Processor processor; + + @Autowired + private MessageCollector messageCollector; + + @Test + @SuppressWarnings("unchecked") + public void testWiring() { + Message<String> message = new GenericMessage<>("hello"); + processor.input().send(message); + Message<String> received = (Message<String>) messageCollector.forChannel(processor.output()).poll(); + assertThat(received.getPayload(), equalTo("hello world")); + } + + + @SpringBootApplication + @EnableBinding(Processor.class) + public static class MyProcessor { + + @Autowired + private Processor channels; + + @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT) + public String transform(String in) { + return in + " world"; + } + } +} +In the preceding example, we create an application that has an input channel and an output channel, both bound through the Processor interface. +The bound interface is injected into the test so that we can have access to both channels. +We send a message on the input channel, and we use the MessageCollector provided by Spring Cloud Stream’s test support to capture that the message has been sent to the output channel as a result. +Once we have received the message, we can validate that the component functions correctly. +
    +Disabling the Test Binder Autoconfiguration +The intent behind the test binder superseding all the other binders on the classpath is to make it easy to test your applications without making changes to your production dependencies. +In some cases (for example, integration tests) it is useful to use the actual production binders instead, and that requires disabling the test binder autoconfiguration. +To do so, you can exclude the org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration class by using one of the Spring Boot autoconfiguration exclusion mechanisms, as shown in the following example: + @SpringBootApplication(exclude = TestSupportBinderAutoConfiguration.class) + @EnableBinding(Processor.class) + public static class MyProcessor { + + @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT) + public String transform(String in) { + return in + " world"; + } + } +When autoconfiguration is disabled, the test binder is available on the classpath, and its defaultCandidate property is set to false so that it does not interfere with the regular user configuration. It can be referenced under the name, test, as shown in the following example: +spring.cloud.stream.defaultBinder=test +
    +
    + +Health Indicator +Spring Cloud Stream provides a health indicator for binders. +It is registered under the name binders and can be enabled or disabled by setting the management.health.binders.enabled property. +By default management.health.binders.enabled is set to false. +Setting management.health.binders.enabled to true enables the health indicator, allowing you to access the /health endpoint to retrieve the binder health indicators. +Health indicators are binder-specific and certain binder implementations may not necessarily provide a health indicator. + + +Metrics Emitter +Spring Boot Actuator provides dependency management and auto-configuration for Micrometer, an application metrics +facade that supports numerous monitoring systems. +Spring Cloud Stream provides support for emitting any available micrometer-based metrics to a binding destination, allowing for periodic +collection of metric data from stream applications without relying on polling individual endpoints. +Metrics Emitter is activated by defining the spring.cloud.stream.bindings.applicationMetrics.destination property, +which specifies the name of the binding destination used by the current binder to publish metric messages. +For example: +spring.cloud.stream.bindings.applicationMetrics.destination=myMetricDestination +The preceding example instructs the binder to bind to myMetricDestination (that is, Rabbit exchange, Kafka topic, and others). +The following properties can be used for customizing the emission of metrics: + + +spring.cloud.stream.metrics.key + +The name of the metric being emitted. Should be a unique value per application. +Default: ${spring.application.name:${vcap.application.name:${spring.config.name:application}}} + + + +spring.cloud.stream.metrics.properties + +Allows white listing application properties that are added to the metrics payload +Default: null. + + + +spring.cloud.stream.metrics.meter-filter + +Pattern to control the 'meters' one wants to capture. +For example, specifying spring.integration.* captures metric information for meters whose name starts with spring.integration. +Default: all 'meters' are captured. + + + +spring.cloud.stream.metrics.schedule-interval + +Interval to control the rate of publishing metric data. +Default: 1 min + + + +Consider the following: +java -jar time-source.jar \ + --spring.cloud.stream.bindings.applicationMetrics.destination=someMetrics \ + --spring.cloud.stream.metrics.properties=spring.application** \ + --spring.cloud.stream.metrics.meter-filter=spring.integration.* +The following example shows the payload of the data published to the binding destination as a result of the preceding command: +{ + "name": "application", + "createdTime": "2018-03-23T14:48:12.700Z", + "properties": { + }, + "metrics": [ + { + "id": { + "name": "spring.integration.send", + "tags": [ + { + "key": "exception", + "value": "none" + }, + { + "key": "name", + "value": "input" + }, + { + "key": "result", + "value": "success" + }, + { + "key": "type", + "value": "channel" + } + ], + "type": "TIMER", + "description": "Send processing time", + "baseUnit": "milliseconds" + }, + "timestamp": "2018-03-23T14:48:12.697Z", + "sum": 130.340546, + "count": 6, + "mean": 21.72342433333333, + "upper": 116.176299, + "total": 130.340546 + } + ] +} + +Given that the format of the Metric message has slightly changed after migrating to Micrometer, the published message will also have +a STREAM_CLOUD_STREAM_VERSION header set to 2.x to help distinguish between Metric messages from the older versions of the Spring Cloud Stream. + + + +Samples +For Spring Cloud Stream samples, see the spring-cloud-stream-samples repository on GitHub. +
    +Deploying Stream Applications on CloudFoundry +On CloudFoundry, services are usually exposed through a special environment variable called VCAP_SERVICES. +When configuring your binder connections, you can use the values from an environment variable as explained on the dataflow Cloud Foundry Server docs. +
    +
    +
    + +Binder Implementations + +Apache Kafka Binder + +
    +Usage +To use Apache Kafka binder, you need to add spring-cloud-stream-binder-kafka as a dependency to your Spring Cloud Stream application, as shown in the following example for Maven: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-binder-kafka</artifactId> +</dependency> +Alternatively, you can also use the Spring Cloud Stream Kafka Starter, as shown inn the following example for Maven: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-stream-kafka</artifactId> +</dependency> +
    +
    +Apache Kafka Binder Overview +The following image shows a simplified diagram of how the Apache Kafka binder operates: +
    +Kafka Binder + + + + +kafka binder + +
    +The Apache Kafka Binder implementation maps each destination to an Apache Kafka topic. +The consumer group maps directly to the same Apache Kafka concept. +Partitioning also maps directly to Apache Kafka partitions as well. +The binder currently uses the Apache Kafka kafka-clients 1.0.0 jar and is designed to be used with a broker of at least that version. +This client can communicate with older brokers (see the Kafka documentation), but certain features may not be available. +For example, with versions earlier than 0.11.x.x, native headers are not supported. +Also, 0.11.x.x does not support the autoAddPartitions property. +
    +
    +Configuration Options +This section contains the configuration options used by the Apache Kafka binder. +For common configuration options and properties pertaining to binder, see the core documentation. +
    +Kafka Binder Properties + + +spring.cloud.stream.kafka.binder.brokers + +A list of brokers to which the Kafka binder connects. +Default: localhost. + + + +spring.cloud.stream.kafka.binder.defaultBrokerPort + +brokers allows hosts specified with or without port information (for example, host1,host2:port2). +This sets the default port when no port is configured in the broker list. +Default: 9092. + + + +spring.cloud.stream.kafka.binder.configuration + +Key/Value map of client properties (both producers and consumer) passed to all clients created by the binder. +Due to the fact that these properties are used by both producers and consumers, usage should be restricted to common properties — for example, security settings. +Properties here supersede any properties set in boot. +Default: Empty map. + + + +spring.cloud.stream.kafka.binder.consumerProperties + +Key/Value map of arbitrary Kafka client consumer properties. +Properties here supersede any properties set in boot and in the configuration property above. +Default: Empty map. + + + +spring.cloud.stream.kafka.binder.headers + +The list of custom headers that are transported by the binder. +Only required when communicating with older applications (⇐ 1.3.x) with a kafka-clients version < 0.11.0.0. Newer versions support headers natively. +Default: empty. + + + +spring.cloud.stream.kafka.binder.healthTimeout + +The time to wait to get partition information, in seconds. +Health reports as down if this timer expires. +Default: 10. + + + +spring.cloud.stream.kafka.binder.requiredAcks + +The number of required acks on the broker. +See the Kafka documentation for the producer acks property. +Default: 1. + + + +spring.cloud.stream.kafka.binder.minPartitionCount + +Effective only if autoCreateTopics or autoAddPartitions is set. +The global minimum number of partitions that the binder configures on topics on which it produces or consumes data. +It can be superseded by the partitionCount setting of the producer or by the value of instanceCount * concurrency settings of the producer (if either is larger). +Default: 1. + + + +spring.cloud.stream.kafka.binder.producerProperties + +Key/Value map of arbitrary Kafka client producer properties. +Properties here supersede any properties set in boot and in the configuration property above. +Default: Empty map. + + + +spring.cloud.stream.kafka.binder.replicationFactor + +The replication factor of auto-created topics if autoCreateTopics is active. +Can be overridden on each binding. +Default: 1. + + + +spring.cloud.stream.kafka.binder.autoCreateTopics + +If set to true, the binder creates new topics automatically. +If set to false, the binder relies on the topics being already configured. +In the latter case, if the topics do not exist, the binder fails to start. + +This setting is independent of the auto.topic.create.enable setting of the broker and does not influence it. +If the server is set to auto-create topics, they may be created as part of the metadata retrieval request, with default broker settings. + +Default: true. + + + +spring.cloud.stream.kafka.binder.autoAddPartitions + +If set to true, the binder creates new partitions if required. +If set to false, the binder relies on the partition size of the topic being already configured. +If the partition count of the target topic is smaller than the expected value, the binder fails to start. +Default: false. + + + +spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix + +Enables transactions in the binder. See transaction.id in the Kafka documentation and Transactions in the spring-kafka documentation. +When transactions are enabled, individual producer properties are ignored and all producers use the spring.cloud.stream.kafka.binder.transaction.producer.* properties. +Default null (no transactions) + + + +spring.cloud.stream.kafka.binder.transaction.producer.* + +Global producer properties for producers in a transactional binder. +See spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix and and the general producer properties supported by all binders. +Default: See individual producer properties. + + + +spring.cloud.stream.kafka.binder.headerMapperBeanName + +The bean name of a KafkaHeaderMapper used for mapping spring-messaging headers to and from Kafka headers. +Use this, for example, if you wish to customize the trusted packages in a DefaultKafkaHeaderMapper that uses JSON deserialization for the headers. +Default: none. + + + +
    +
    +Kafka Consumer Properties +The following properties are available for Kafka consumers only and +must be prefixed with spring.cloud.stream.kafka.bindings.<channelName>.consumer.. + + +admin.configuration + +A Map of Kafka topic properties used when provisioning topics — for example, spring.cloud.stream.kafka.bindings.input.consumer.admin.configuration.message.format.version=0.9.0.0 +Default: none. + + + +admin.replicas-assignment + +A Map<Integer, List<Integer>> of replica assignments, with the key being the partition and the value being the assignments. +Used when provisioning new topics. +See the NewTopic Javadocs in the kafka-clients jar. +Default: none. + + + +admin.replication-factor + +The replication factor to use when provisioning topics. Overrides the binder-wide setting. +Ignored if replicas-assignments is present. +Default: none (the binder-wide default of 1 is used). + + + +autoRebalanceEnabled + +When true, topic partitions is automatically rebalanced between the members of a consumer group. +When false, each consumer is assigned a fixed set of partitions based on spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex. +This requires both the spring.cloud.stream.instanceCount and spring.cloud.stream.instanceIndex properties to be set appropriately on each launched instance. +The value of the spring.cloud.stream.instanceCount property must typically be greater than 1 in this case. +Default: true. + + + +ackEachRecord + +When autoCommitOffset is true, this setting dictates whether to commit the offset after each record is processed. +By default, offsets are committed after all records in the batch of records returned by consumer.poll() have been processed. +The number of records returned by a poll can be controlled with the max.poll.records Kafka property, which is set through the consumer configuration property. +Setting this to true may cause a degradation in performance, but doing so reduces the likelihood of redelivered records when a failure occurs. +Also, see the binder requiredAcks property, which also affects the performance of committing offsets. +Default: false. + + + +autoCommitOffset + +Whether to autocommit offsets when a message has been processed. +If set to false, a header with the key kafka_acknowledgment of the type org.springframework.kafka.support.Acknowledgment header is present in the inbound message. +Applications may use this header for acknowledging messages. +See the examples section for details. +When this property is set to false, Kafka binder sets the ack mode to org.springframework.kafka.listener.AbstractMessageListenerContainer.AckMode.MANUAL and the application is responsible for acknowledging records. +Also see ackEachRecord. +Default: true. + + + +autoCommitOnError + +Effective only if autoCommitOffset is set to true. +If set to false, it suppresses auto-commits for messages that result in errors and commits only for successful messages. It allows a stream to automatically replay from the last successfully processed message, in case of persistent failures. +If set to true, it always auto-commits (if auto-commit is enabled). +If not set (the default), it effectively has the same value as enableDlq, auto-committing erroneous messages if they are sent to a DLQ and not committing them otherwise. +Default: not set. + + + +resetOffsets + +Whether to reset offsets on the consumer to the value provided by startOffset. +Default: false. + + + +startOffset + +The starting offset for new groups. +Allowed values: earliest and latest. +If the consumer group is set explicitly for the consumer 'binding' (through spring.cloud.stream.bindings.<channelName>.group), 'startOffset' is set to earliest. Otherwise, it is set to latest for the anonymous consumer group. +Also see resetOffsets (earlier in this list). +Default: null (equivalent to earliest). + + + +enableDlq + +When set to true, it enables DLQ behavior for the consumer. +By default, messages that result in errors are forwarded to a topic named error.<destination>.<group>. +The DLQ topic name can be configurable by setting the dlqName property. +This provides an alternative option to the more common Kafka replay scenario for the case when the number of errors is relatively small and replaying the entire original topic may be too cumbersome. +See processing for more information. +Starting with version 2.0, messages sent to the DLQ topic are enhanced with the following headers: x-original-topic, x-exception-message, and x-exception-stacktrace as byte[]. +Not allowed when destinationIsPattern is true. +Default: false. + + + +configuration + +Map with a key/value pair containing generic Kafka consumer properties. +Default: Empty map. + + + +dlqName + +The name of the DLQ topic to receive the error messages. +Default: null (If not specified, messages that result in errors are forwarded to a topic named error.<destination>.<group>). + + + +dlqProducerProperties + +Using this, DLQ-specific producer properties can be set. +All the properties available through kafka producer properties can be set through this property. +Default: Default Kafka producer properties. + + + +standardHeaders + +Indicates which standard headers are populated by the inbound channel adapter. +Allowed values: none, id, timestamp, or both. +Useful if using native deserialization and the first component to receive a message needs an id (such as an aggregator that is configured to use a JDBC message store). +Default: none + + + +converterBeanName + +The name of a bean that implements RecordMessageConverter. Used in the inbound channel adapter to replace the default MessagingMessageConverter. +Default: null + + + +idleEventInterval + +The interval, in milliseconds, between events indicating that no messages have recently been received. +Use an ApplicationListener<ListenerContainerIdleEvent> to receive these events. +See for a usage example. +Default: 30000 + + + +destinationIsPattern + +When true, the destination is treated as a regular expression Pattern used to match topic names by the broker. +When true, topics are not provisioned, and enableDlq is not allowed, because the binder does not know the topic names during the provisioning phase. +Note, the time taken to detect new topics that match the pattern is controlled by the consumer property metadata.max.age.ms, which (at the time of writing) defaults to 300,000ms (5 minutes). +This can be configured using the configuration property above. +Default: false + + + +
    +
    +Kafka Producer Properties +The following properties are available for Kafka producers only and +must be prefixed with spring.cloud.stream.kafka.bindings.<channelName>.producer.. + + +admin.configuration + +A Map of Kafka topic properties used when provisioning new topics — for example, spring.cloud.stream.kafka.bindings.input.consumer.admin.configuration.message.format.version=0.9.0.0 +Default: none. + + + +admin.replicas-assignment + +A Map<Integer, List<Integer>> of replica assignments, with the key being the partition and the value being the assignments. +Used when provisioning new topics. +See NewTopic javadocs in the kafka-clients jar. +Default: none. + + + +admin.replication-factor + +The replication factor to use when provisioning new topics. Overrides the binder-wide setting. +Ignored if replicas-assignments is present. +Default: none (the binder-wide default of 1 is used). + + + +bufferSize + +Upper limit, in bytes, of how much data the Kafka producer attempts to batch before sending. +Default: 16384. + + + +sync + +Whether the producer is synchronous. +Default: false. + + + +batchTimeout + +How long the producer waits to allow more messages to accumulate in the same batch before sending the messages. +(Normally, the producer does not wait at all and simply sends all the messages that accumulated while the previous send was in progress.) A non-zero value may increase throughput at the expense of latency. +Default: 0. + + + +messageKeyExpression + +A SpEL expression evaluated against the outgoing message used to populate the key of the produced Kafka message — for example, headers['myKey']. +The payload cannot be used because, by the time this expression is evaluated, the payload is already in the form of a byte[]. +Default: none. + + + +headerPatterns + +A comma-delimited list of simple patterns to match Spring messaging headers to be mapped to the Kafka Headers in the ProducerRecord. +Patterns can begin or end with the wildcard character (asterisk). +Patterns can be negated by prefixing with !. +Matching stops after the first match (positive or negative). +For example !ask,as* will pass ash but not ask. +id and timestamp are never mapped. +Default: * (all headers - except the id and timestamp) + + + +configuration + +Map with a key/value pair containing generic Kafka producer properties. +Default: Empty map. + + + + +The Kafka binder uses the partitionCount setting of the producer as a hint to create a topic with the given partition count (in conjunction with the minPartitionCount, the maximum of the two being the value being used). +Exercise caution when configuring both minPartitionCount for a binder and partitionCount for an application, as the larger value is used. +If a topic already exists with a smaller partition count and autoAddPartitions is disabled (the default), the binder fails to start. +If a topic already exists with a smaller partition count and autoAddPartitions is enabled, new partitions are added. +If a topic already exists with a larger number of partitions than the maximum of (minPartitionCount or partitionCount), the existing partition count is used. + +
    +
    +Usage examples +In this section, we show the use of the preceding properties for specific scenarios. +
    +Example: Setting <literal>autoCommitOffset</literal> to <literal>false</literal> and Relying on Manual Acking +This example illustrates how one may manually acknowledge offsets in a consumer application. +This example requires that spring.cloud.stream.kafka.bindings.input.consumer.autoCommitOffset be set to false. +Use the corresponding input channel name for your example. +@SpringBootApplication +@EnableBinding(Sink.class) +public class ManuallyAcknowdledgingConsumer { + + public static void main(String[] args) { + SpringApplication.run(ManuallyAcknowdledgingConsumer.class, args); + } + + @StreamListener(Sink.INPUT) + public void process(Message<?> message) { + Acknowledgment acknowledgment = message.getHeaders().get(KafkaHeaders.ACKNOWLEDGMENT, Acknowledgment.class); + if (acknowledgment != null) { + System.out.println("Acknowledgment provided"); + acknowledgment.acknowledge(); + } + } +} +
    +
    +Example: Security Configuration +Apache Kafka 0.9 supports secure connections between client and brokers. +To take advantage of this feature, follow the guidelines in the Apache Kafka Documentation as well as the Kafka 0.9 security guidelines from the Confluent documentation. +Use the spring.cloud.stream.kafka.binder.configuration option to set security properties for all clients created by the binder. +For example, to set security.protocol to SASL_SSL, set the following property: +spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_SSL +All the other security properties can be set in a similar manner. +When using Kerberos, follow the instructions in the reference documentation for creating and referencing the JAAS configuration. +Spring Cloud Stream supports passing JAAS configuration information to the application by using a JAAS configuration file and using Spring Boot properties. +
    +Using JAAS Configuration Files +The JAAS and (optionally) krb5 file locations can be set for Spring Cloud Stream applications by using system properties. +The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using a JAAS configuration file: + java -Djava.security.auth.login.config=/path.to/kafka_client_jaas.conf -jar log.jar \ + --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \ + --spring.cloud.stream.bindings.input.destination=stream.ticktock \ + --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT +
    +
    +Using Spring Boot Properties +As an alternative to having a JAAS configuration file, Spring Cloud Stream provides a mechanism for setting up the JAAS configuration for Spring Cloud Stream applications by using Spring Boot properties. +The following properties can be used to configure the login context of the Kafka client: + + +spring.cloud.stream.kafka.binder.jaas.loginModule + +The login module name. Not necessary to be set in normal cases. +Default: com.sun.security.auth.module.Krb5LoginModule. + + + +spring.cloud.stream.kafka.binder.jaas.controlFlag + +The control flag of the login module. +Default: required. + + + +spring.cloud.stream.kafka.binder.jaas.options + +Map with a key/value pair containing the login module options. +Default: Empty map. + + + +The following example shows how to launch a Spring Cloud Stream application with SASL and Kerberos by using Spring Boot configuration properties: + java --spring.cloud.stream.kafka.binder.brokers=secure.server:9092 \ + --spring.cloud.stream.bindings.input.destination=stream.ticktock \ + --spring.cloud.stream.kafka.binder.autoCreateTopics=false \ + --spring.cloud.stream.kafka.binder.configuration.security.protocol=SASL_PLAINTEXT \ + --spring.cloud.stream.kafka.binder.jaas.options.useKeyTab=true \ + --spring.cloud.stream.kafka.binder.jaas.options.storeKey=true \ + --spring.cloud.stream.kafka.binder.jaas.options.keyTab=/etc/security/keytabs/kafka_client.keytab \ + --spring.cloud.stream.kafka.binder.jaas.options.principal=kafka-client-1@EXAMPLE.COM +The preceding example represents the equivalent of the following JAAS file: +KafkaClient { + com.sun.security.auth.module.Krb5LoginModule required + useKeyTab=true + storeKey=true + keyTab="/etc/security/keytabs/kafka_client.keytab" + principal="kafka-client-1@EXAMPLE.COM"; +}; +If the topics required already exist on the broker or will be created by an administrator, autocreation can be turned off and only client JAAS properties need to be sent. + +Do not mix JAAS configuration files and Spring Boot properties in the same application. +If the -Djava.security.auth.login.config system property is already present, Spring Cloud Stream ignores the Spring Boot properties. + + +Be careful when using the autoCreateTopics and autoAddPartitions with Kerberos. +Usually, applications may use principals that do not have administrative rights in Kafka and Zookeeper. +Consequently, relying on Spring Cloud Stream to create/modify topics may fail. +In secure environments, we strongly recommend creating topics and managing ACLs administratively by using Kafka tooling. + +
    +
    +
    +Example: Pausing and Resuming the Consumer +If you wish to suspend consumption but not cause a partition rebalance, you can pause and resume the consumer. +This is facilitated by adding the Consumer as a parameter to your @StreamListener. +To resume, you need an ApplicationListener for ListenerContainerIdleEvent instances. +The frequency at which events are published is controlled by the idleEventInterval property. +Since the consumer is not thread-safe, you must call these methods on the calling thread. +The following simple application shows how to pause and resume: +@SpringBootApplication +@EnableBinding(Sink.class) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @StreamListener(Sink.INPUT) + public void in(String in, @Header(KafkaHeaders.CONSUMER) Consumer<?, ?> consumer) { + System.out.println(in); + consumer.pause(Collections.singleton(new TopicPartition("myTopic", 0))); + } + + @Bean + public ApplicationListener<ListenerContainerIdleEvent> idleListener() { + return event -> { + System.out.println(event); + if (event.getConsumer().paused().size() > 0) { + event.getConsumer().resume(event.getConsumer().paused()); + } + }; + } + +} +
    +
    +
    +
    +Error Channels +Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See for more information. +The payload of the ErrorMessage for a send failure is a KafkaSendFailureException with properties: + + +failedMessage: The Spring Messaging Message<?> that failed to be sent. + + +record: The raw ProducerRecord that was created from the failedMessage + + +There is no automatic handling of producer exceptions (such as sending to a Dead-Letter queue). +You can consume these exceptions with your own Spring Integration flow. +
    +
    +Kafka Metrics +Kafka binder module exposes the following metrics: +spring.cloud.stream.binder.kafka.offset: This metric indicates how many messages have not been yet consumed from a given binder’s topic by a given consumer group. +The metrics provided are based on the Mircometer metrics library. The metric contains the consumer group information, topic and the actual lag in committed offset from the latest offset on the topic. +This metric is particularly useful for providing auto-scaling feedback to a PaaS platform. +
    +
    +Dead-Letter Topic Processing +Because you cannot anticipate how users would want to dispose of dead-lettered messages, the framework does not provide any standard mechanism to handle them. +If the reason for the dead-lettering is transient, you may wish to route the messages back to the original topic. +However, if the problem is a permanent issue, that could cause an infinite loop. +The sample Spring Boot application within this topic is an example of how to route those messages back to the original topic, but it moves them to a parking lot topic after three attempts. +The application is another spring-cloud-stream application that reads from the dead-letter topic. +It terminates when no messages are received for 5 seconds. +The examples assume the original destination is so8400out and the consumer group is so8400. +There are a couple of strategies to consider: + + +Consider running the rerouting only when the main application is not running. +Otherwise, the retries for transient errors are used up very quickly. + + +Alternatively, use a two-stage approach: Use this application to route to a third topic and another to route from there back to the main topic. + + +The following code listings show the sample application: + +application.properties + +spring.cloud.stream.bindings.input.group=so8400replay +spring.cloud.stream.bindings.input.destination=error.so8400out.so8400 + +spring.cloud.stream.bindings.output.destination=so8400out +spring.cloud.stream.bindings.output.producer.partitioned=true + +spring.cloud.stream.bindings.parkingLot.destination=so8400in.parkingLot +spring.cloud.stream.bindings.parkingLot.producer.partitioned=true + +spring.cloud.stream.kafka.binder.configuration.auto.offset.reset=earliest + +spring.cloud.stream.kafka.binder.headers=x-retries + + + +Application + +@SpringBootApplication +@EnableBinding(TwoOutputProcessor.class) +public class ReRouteDlqKApplication implements CommandLineRunner { + + private static final String X_RETRIES_HEADER = "x-retries"; + + public static void main(String[] args) { + SpringApplication.run(ReRouteDlqKApplication.class, args).close(); + } + + private final AtomicInteger processed = new AtomicInteger(); + + @Autowired + private MessageChannel parkingLot; + + @StreamListener(Processor.INPUT) + @SendTo(Processor.OUTPUT) + public Message<?> reRoute(Message<?> failed) { + processed.incrementAndGet(); + Integer retries = failed.getHeaders().get(X_RETRIES_HEADER, Integer.class); + if (retries == null) { + System.out.println("First retry for " + failed); + return MessageBuilder.fromMessage(failed) + .setHeader(X_RETRIES_HEADER, new Integer(1)) + .setHeader(BinderHeaders.PARTITION_OVERRIDE, + failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID)) + .build(); + } + else if (retries.intValue() < 3) { + System.out.println("Another retry for " + failed); + return MessageBuilder.fromMessage(failed) + .setHeader(X_RETRIES_HEADER, new Integer(retries.intValue() + 1)) + .setHeader(BinderHeaders.PARTITION_OVERRIDE, + failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID)) + .build(); + } + else { + System.out.println("Retries exhausted for " + failed); + parkingLot.send(MessageBuilder.fromMessage(failed) + .setHeader(BinderHeaders.PARTITION_OVERRIDE, + failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID)) + .build()); + } + return null; + } + + @Override + public void run(String... args) throws Exception { + while (true) { + int count = this.processed.get(); + Thread.sleep(5000); + if (count == this.processed.get()) { + System.out.println("Idle, terminating"); + return; + } + } + } + + public interface TwoOutputProcessor extends Processor { + + @Output("parkingLot") + MessageChannel parkingLot(); + + } + +} + + +
    +
    +Partitioning with the Kafka Binder +Apache Kafka supports topic partitioning natively. +Sometimes it is advantageous to send data to specific partitions — for example, when you want to strictly order message processing (all messages for a particular customer should go to the same partition). +The following example shows how to configure the producer and consumer side: +@SpringBootApplication +@EnableBinding(Source.class) +public class KafkaPartitionProducerApplication { + + private static final Random RANDOM = new Random(System.currentTimeMillis()); + + private static final String[] data = new String[] { + "foo1", "bar1", "qux1", + "foo2", "bar2", "qux2", + "foo3", "bar3", "qux3", + "foo4", "bar4", "qux4", + }; + + public static void main(String[] args) { + new SpringApplicationBuilder(KafkaPartitionProducerApplication.class) + .web(false) + .run(args); + } + + @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedRate = "5000")) + public Message<?> generate() { + String value = data[RANDOM.nextInt(data.length)]; + System.out.println("Sending: " + value); + return MessageBuilder.withPayload(value) + .setHeader("partitionKey", value) + .build(); + } + +} + +application.yml + +spring: + cloud: + stream: + bindings: + output: + destination: partitioned.topic + producer: + partitioned: true + partition-key-expression: headers['partitionKey'] + partition-count: 12 + + + +The topic must be provisioned to have enough partitions to achieve the desired concurrency for all consumer groups. +The above configuration supports up to 12 consumer instances (6 if their concurrency is 2, 4 if their concurrency is 3, and so on). +It is generally best to over-provision the partitions to allow for future increases in consumers or concurrency. + + +The preceding configuration uses the default partitioning (key.hashCode() % partitionCount). +This may or may not provide a suitably balanced algorithm, depending on the key values. +You can override this default by using the partitionSelectorExpression or partitionSelectorClass properties. + +Since partitions are natively handled by Kafka, no special configuration is needed on the consumer side. +Kafka allocates partitions across the instances. +The following Spring Boot application listens to a Kafka stream and prints (to the console) the partition ID to which each message goes: +@SpringBootApplication +@EnableBinding(Sink.class) +public class KafkaPartitionConsumerApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(KafkaPartitionConsumerApplication.class) + .web(false) + .run(args); + } + + @StreamListener(Sink.INPUT) + public void listen(@Payload String in, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition) { + System.out.println(in + " received from partition " + partition); + } + +} + +application.yml + +spring: + cloud: + stream: + bindings: + input: + destination: partitioned.topic + group: myGroup + + +You can add instances as needed. +Kafka rebalances the partition allocations. +If the instance count (or instance count * concurrency) exceeds the number of partitions, some consumers are idle. +
    +
    + +Apache Kafka Streams Binder +
    +Usage +For using the Kafka Streams binder, you just need to add it to your Spring Cloud Stream application, using the following +Maven coordinates: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-binder-kafka-streams</artifactId> +</dependency> +
    +
    +Kafka Streams Binder Overview +Spring Cloud Stream’s Apache Kafka support also includes a binder implementation designed explicitly for Apache Kafka +Streams binding. With this native integration, a Spring Cloud Stream "processor" application can directly use the +Apache Kafka Streams APIs in the core business logic. +Kafka Streams binder implementation builds on the foundation provided by the Kafka Streams in Spring Kafka +project. +Kafka Streams binder provides binding capabilities for the three major types in Kafka Streams - KStream, KTable and GlobalKTable. +As part of this native integration, the high-level Streams DSL +provided by the Kafka Streams API is available for use in the business logic. +An early version of the Processor API +support is available as well. +As noted early-on, Kafka Streams support in Spring Cloud Stream is strictly only available for use in the Processor model. +A model in which the messages read from an inbound topic, business processing can be applied, and the transformed messages +can be written to an outbound topic. It can also be used in Processor applications with a no-outbound destination. +
    +Streams DSL +This application consumes data from a Kafka topic (e.g., words), computes word count for each unique word in a 5 seconds +time window, and the computed results are sent to a downstream topic (e.g., counts) for further processing. +@SpringBootApplication +@EnableBinding(KStreamProcessor.class) +public class WordCountProcessorApplication { + + @StreamListener("input") + @SendTo("output") + public KStream<?, WordCount> process(KStream<?, String> input) { + return input + .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+"))) + .groupBy((key, value) -> value) + .windowedBy(TimeWindows.of(5000)) + .count(Materialized.as("WordCounts-multi")) + .toStream() + .map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, new Date(key.window().start()), new Date(key.window().end())))); + } + + public static void main(String[] args) { + SpringApplication.run(WordCountProcessorApplication.class, args); + } +Once built as a uber-jar (e.g., wordcount-processor.jar), you can run the above example like the following. +java -jar wordcount-processor.jar --spring.cloud.stream.bindings.input.destination=words --spring.cloud.stream.bindings.output.destination=counts +This application will consume messages from the Kafka topic words and the computed results are published to an output +topic counts. +Spring Cloud Stream will ensure that the messages from both the incoming and outgoing topics are automatically bound as +KStream objects. As a developer, you can exclusively focus on the business aspects of the code, i.e. writing the logic +required in the processor. Setting up the Streams DSL specific configuration required by the Kafka Streams infrastructure +is automatically handled by the framework. +
    +
    +
    +Configuration Options +This section contains the configuration options used by the Kafka Streams binder. +For common configuration options and properties pertaining to binder, refer to the core documentation. +
    +Kafka Streams Properties +The following properties are available at the binder level and must be prefixed with spring.cloud.stream.kafka.streams.binder. +literal. + + +configuration + + Map with a key/value pair containing properties pertaining to Apache Kafka Streams API. + This property must be prefixed with spring.cloud.stream.kafka.streams.binder.. +Following are some examples of using this property. + + + +spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=org.apache.kafka.common.serialization.Serdes$StringSerde +spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=org.apache.kafka.common.serialization.Serdes$StringSerde +spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000 +For more information about all the properties that may go into streams configuration, see StreamsConfig JavaDocs in +Apache Kafka Streams docs. + + +brokers + +Broker URL +Default: localhost + + + +zkNodes + +Zookeeper URL +Default: localhost + + + +serdeError + +Deserialization error handler type. +Possible values are - logAndContinue, logAndFail or sendToDlq +Default: logAndFail + + + +applicationId + +Convenient way to set the application.id for the Kafka Streams application globally at the binder level. +If the application contains multiple StreamListener methods, then application.id should be set at the binding level per input binding. +Default: none + + + +The following properties are only available for Kafka Streams producers and must be prefixed with spring.cloud.stream.kafka.streams.bindings.<binding name>.producer. literal. +For convenience, if there multiple output bindings and they all require a common value, that can be configured by using the prefix spring.cloud.stream.kafka.streams.default.producer.. + + +keySerde + +key serde to use +Default: none. + + + +valueSerde + +value serde to use +Default: none. + + + +useNativeEncoding + +flag to enable native encoding +Default: false. + + + +The following properties are only available for Kafka Streams consumers and must be prefixed with spring.cloud.stream.kafka.streams.bindings.<binding name>.consumer.`literal. +For convenience, if there multiple input bindings and they all require a common value, that can be configured by using the prefix `spring.cloud.stream.kafka.streams.default.consumer.. + + +applicationId + +Setting application.id per input binding. +Default: none + + + +keySerde + +key serde to use +Default: none. + + + +valueSerde + +value serde to use +Default: none. + + + +materializedAs + +state store to materialize when using incoming KTable types +Default: none. + + + +useNativeDecoding + +flag to enable native decoding +Default: false. + + + +dlqName + +DLQ topic name. +Default: none. + + + +
    +
    +TimeWindow properties: +Windowing is an important concept in stream processing applications. Following properties are available to configure +time-window computations. + + +spring.cloud.stream.kafka.streams.timeWindow.length + +When this property is given, you can autowire a TimeWindows bean into the application. +The value is expressed in milliseconds. +Default: none. + + + +spring.cloud.stream.kafka.streams.timeWindow.advanceBy + +Value is given in milliseconds. +Default: none. + + + +
    +
    +
    +Multiple Input Bindings +For use cases that requires multiple incoming KStream objects or a combination of KStream and KTable objects, the Kafka +Streams binder provides multiple bindings support. +Let’s see it in action. +
    +Multiple Input Bindings as a Sink +@EnableBinding(KStreamKTableBinding.class) +..... +..... +@StreamListener +public void process(@Input("inputStream") KStream<String, PlayEvent> playEvents, + @Input("inputTable") KTable<Long, Song> songTable) { + .... + .... +} + +interface KStreamKTableBinding { + + @Input("inputStream") + KStream<?, ?> inputStream(); + + @Input("inputTable") + KTable<?, ?> inputTable(); +} +In the above example, the application is written as a sink, i.e. there are no output bindings and the application has to +decide concerning downstream processing. When you write applications in this style, you might want to send the information +downstream or store them in a state store (See below for Queryable State Stores). +In the case of incoming KTable, if you want to materialize the computations to a state store, you have to express it +through the following property. +spring.cloud.stream.kafka.streams.bindings.inputTable.consumer.materializedAs: all-songs +The above example shows the use of KTable as an input binding. +The binder also supports input bindings for GlobalKTable. +GlobalKTable binding is useful when you have to ensure that all instances of your application has access to the data updates from the topic. +KTable and GlobalKTable bindings are only available on the input. +Binder supports both input and output bindings for KStream. +
    +
    +Multiple Input Bindings as a Processor +@EnableBinding(KStreamKTableBinding.class) +.... +.... + +@StreamListener +@SendTo("output") +public KStream<String, Long> process(@Input("input") KStream<String, Long> userClicksStream, + @Input("inputTable") KTable<String, String> userRegionsTable) { +.... +.... +} + +interface KStreamKTableBinding extends KafkaStreamsProcessor { + + @Input("inputX") + KTable<?, ?> inputTable(); +} +
    +
    +
    +Multiple Output Bindings (aka Branching) +Kafka Streams allow outbound data to be split into multiple topics based on some predicates. The Kafka Streams binder provides +support for this feature without compromising the programming model exposed through StreamListener in the end user application. +You can write the application in the usual way as demonstrated above in the word count example. However, when using the +branching feature, you are required to do a few things. First, you need to make sure that your return type is KStream[] +instead of a regular KStream. Second, you need to use the SendTo annotation containing the output bindings in the order +(see example below). For each of these output bindings, you need to configure destination, content-type etc., complying with +the standard Spring Cloud Stream expectations. +Here is an example: +@EnableBinding(KStreamProcessorWithBranches.class) +@EnableAutoConfiguration +public static class WordCountProcessorApplication { + + @Autowired + private TimeWindows timeWindows; + + @StreamListener("input") + @SendTo({"output1","output2","output3}) + public KStream<?, WordCount>[] process(KStream<Object, String> input) { + + Predicate<Object, WordCount> isEnglish = (k, v) -> v.word.equals("english"); + Predicate<Object, WordCount> isFrench = (k, v) -> v.word.equals("french"); + Predicate<Object, WordCount> isSpanish = (k, v) -> v.word.equals("spanish"); + + return input + .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+"))) + .groupBy((key, value) -> value) + .windowedBy(timeWindows) + .count(Materialized.as("WordCounts-1")) + .toStream() + .map((key, value) -> new KeyValue<>(null, new WordCount(key.key(), value, new Date(key.window().start()), new Date(key.window().end())))) + .branch(isEnglish, isFrench, isSpanish); + } + + interface KStreamProcessorWithBranches { + + @Input("input") + KStream<?, ?> input(); + + @Output("output1") + KStream<?, ?> output1(); + + @Output("output2") + KStream<?, ?> output2(); + + @Output("output3") + KStream<?, ?> output3(); + } +} +Properties: +spring.cloud.stream.bindings.output1.contentType: application/json +spring.cloud.stream.bindings.output2.contentType: application/json +spring.cloud.stream.bindings.output3.contentType: application/json +spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms: 1000 +spring.cloud.stream.kafka.streams.binder.configuration: + default.key.serde: org.apache.kafka.common.serialization.Serdes$StringSerde + default.value.serde: org.apache.kafka.common.serialization.Serdes$StringSerde +spring.cloud.stream.bindings.output1: + destination: foo + producer: + headerMode: raw +spring.cloud.stream.bindings.output2: + destination: bar + producer: + headerMode: raw +spring.cloud.stream.bindings.output3: + destination: fox + producer: + headerMode: raw +spring.cloud.stream.bindings.input: + destination: words + consumer: + headerMode: raw +
    +
    +Message Conversion +Similar to message-channel based binder applications, the Kafka Streams binder adapts to the out-of-the-box content-type +conversions without any compromise. +It is typical for Kafka Streams operations to know the type of SerDe’s used to transform the key and value correctly. +Therefore, it may be more natural to rely on the SerDe facilities provided by the Apache Kafka Streams library itself at +the inbound and outbound conversions rather than using the content-type conversions offered by the framework. +On the other hand, you might be already familiar with the content-type conversion patterns provided by the framework, and +that, you’d like to continue using for inbound and outbound conversions. +Both the options are supported in the Kafka Streams binder implementation. +
    +Outbound serialization +If native encoding is disabled (which is the default), then the framework will convert the message using the contentType +set by the user (otherwise, the default application/json will be applied). It will ignore any SerDe set on the outbound +in this case for outbound serialization. +Here is the property to set the contentType on the outbound. +spring.cloud.stream.bindings.output.contentType: application/json +Here is the property to enable native encoding. +spring.cloud.stream.bindings.output.nativeEncoding: true +If native encoding is enabled on the output binding (user has to enable it as above explicitly), then the framework will +skip any form of automatic message conversion on the outbound. In that case, it will switch to the Serde set by the user. +The valueSerde property set on the actual output binding will be used. Here is an example. +spring.cloud.stream.kafka.streams.bindings.output.producer.valueSerde: org.apache.kafka.common.serialization.Serdes$StringSerde +If this property is not set, then it will use the "default" SerDe: spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde. +It is worth to mention that Kafka Streams binder does not serialize the keys on outbound - it simply relies on Kafka itself. +Therefore, you either have to specify the keySerde property on the binding or it will default to the application-wide common +keySerde. +Binding level key serde: +spring.cloud.stream.kafka.streams.bindings.output.producer.keySerde +Common Key serde: +spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde +If branching is used, then you need to use multiple output bindings. For example, +interface KStreamProcessorWithBranches { + + @Input("input") + KStream<?, ?> input(); + + @Output("output1") + KStream<?, ?> output1(); + + @Output("output2") + KStream<?, ?> output2(); + + @Output("output3") + KStream<?, ?> output3(); + } +If nativeEncoding is set, then you can set different SerDe’s on individual output bindings as below. +spring.cloud.stream.kafka.streams.bindings.output1.producer.valueSerde=IntegerSerde +spring.cloud.stream.kafka.streams.bindings.output2.producer.valueSerde=StringSerde +spring.cloud.stream.kafka.streams.bindings.output3.producer.valueSerde=JsonSerde +Then if you have SendTo like this, @SendTo({"output1", "output2", "output3"}), the KStream[] from the branches are +applied with proper SerDe objects as defined above. If you are not enabling nativeEncoding, you can then set different +contentType values on the output bindings as below. In that case, the framework will use the appropriate message converter +to convert the messages before sending to Kafka. +spring.cloud.stream.bindings.output1.contentType: application/json +spring.cloud.stream.bindings.output2.contentType: application/java-serialzied-object +spring.cloud.stream.bindings.output3.contentType: application/octet-stream +
    +
    +Inbound Deserialization +Similar rules apply to data deserialization on the inbound. +If native decoding is disabled (which is the default), then the framework will convert the message using the contentType +set by the user (otherwise, the default application/json will be applied). It will ignore any SerDe set on the inbound +in this case for inbound deserialization. +Here is the property to set the contentType on the inbound. +spring.cloud.stream.bindings.input.contentType: application/json +Here is the property to enable native decoding. +spring.cloud.stream.bindings.input.nativeDecoding: true +If native decoding is enabled on the input binding (user has to enable it as above explicitly), then the framework will +skip doing any message conversion on the inbound. In that case, it will switch to the SerDe set by the user. The valueSerde +property set on the actual output binding will be used. Here is an example. +spring.cloud.stream.kafka.streams.bindings.input.consumer.valueSerde: org.apache.kafka.common.serialization.Serdes$StringSerde +If this property is not set, it will use the default SerDe: spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde. +It is worth to mention that Kafka Streams binder does not deserialize the keys on inbound - it simply relies on Kafka itself. +Therefore, you either have to specify the keySerde property on the binding or it will default to the application-wide common +keySerde. +Binding level key serde: +spring.cloud.stream.kafka.streams.bindings.input.consumer.keySerde +Common Key serde: +spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde +As in the case of KStream branching on the outbound, the benefit of setting value SerDe per binding is that if you have +multiple input bindings (multiple KStreams object) and they all require separate value SerDe’s, then you can configure +them individually. If you use the common configuration approach, then this feature won’t be applicable. +
    +
    +
    +Error Handling +Apache Kafka Streams provide the capability for natively handling exceptions from deserialization errors. +For details on this support, please see this +Out of the box, Apache Kafka Streams provide two kinds of deserialization exception handlers - logAndContinue and logAndFail. +As the name indicates, the former will log the error and continue processing the next records and the latter will log the +error and fail. LogAndFail is the default deserialization exception handler. +
    +Handling Deserialization Exceptions +Kafka Streams binder supports a selection of exception handlers through the following properties. +spring.cloud.stream.kafka.streams.binder.serdeError: logAndContinue +In addition to the above two deserialization exception handlers, the binder also provides a third one for sending the erroneous +records (poison pills) to a DLQ topic. Here is how you enable this DLQ exception handler. +spring.cloud.stream.kafka.streams.binder.serdeError: sendToDlq +When the above property is set, all the deserialization error records are automatically sent to the DLQ topic. +spring.cloud.stream.kafka.streams.bindings.input.consumer.dlqName: foo-dlq +If this is set, then the error records are sent to the topic foo-dlq. If this is not set, then it will create a DLQ +topic with the name error.<input-topic-name>.<group-name>. +A couple of things to keep in mind when using the exception handling feature in Kafka Streams binder. + + +The property spring.cloud.stream.kafka.streams.binder.serdeError is applicable for the entire application. This implies +that if there are multiple StreamListener methods in the same application, this property is applied to all of them. + + +The exception handling for deserialization works consistently with native deserialization and framework provided message +conversion. + + +
    +
    +Handling Non-Deserialization Exceptions +For general error handling in Kafka Streams binder, it is up to the end user applications to handle application level errors. +As a side effect of providing a DLQ for deserialization exception handlers, Kafka Streams binder provides a way to get +access to the DLQ sending bean directly from your application. +Once you get access to that bean, you can programmatically send any exception records from your application to the DLQ. +It continues to remain hard to robust error handling using the high-level DSL; Kafka Streams doesn’t natively support error +handling yet. +However, when you use the low-level Processor API in your application, there are options to control this behavior. See +below. +@Autowired +private SendToDlqAndContinue dlqHandler; + +@StreamListener("input") +@SendTo("output") +public KStream<?, WordCount> process(KStream<Object, String> input) { + + input.process(() -> new Processor() { + ProcessorContext context; + + @Override + public void init(ProcessorContext context) { + this.context = context; + } + + @Override + public void process(Object o, Object o2) { + + try { + ..... + ..... + } + catch(Exception e) { + //explicitly provide the kafka topic corresponding to the input binding as the first argument. + //DLQ handler will correctly map to the dlq topic from the actual incoming destination. + dlqHandler.sendToDlq("topic-name", (byte[]) o1, (byte[]) o2, context.partition()); + } + } + + ..... + ..... + }); +} +
    +
    +
    +State Store +State store is created automatically by Kafka Streams when the DSL is used. +When processor API is used, you need to register a state store manually. In order to do so, you can use KafkaStreamsStateStore annotation. +You can specify the name and type of the store, flags to control log and disabling cache, etc. +Once the store is created by the binder during the bootstrapping phase, you can access this state store through the processor API. +Below are some primitives for doing this. +Creating a state store: +@KafkaStreamsStateStore(name="mystate", type= KafkaStreamsStateStoreProperties.StoreType.WINDOW, lengthMs=300000) +public void process(KStream<Object, Product> input) { + ... +} +Accessing the state store: +Processor<Object, Product>() { + + WindowStore<Object, String> state; + + @Override + public void init(ProcessorContext processorContext) { + state = (WindowStore)processorContext.getStateStore("mystate"); + } + ... +} +
    +
    +Interactive Queries +As part of the public Kafka Streams binder API, we expose a class called InteractiveQueryService. +You can access this as a Spring bean in your application. An easy way to get access to this bean from your application is to "autowire" the bean. +@Autowired +private InteractiveQueryService interactiveQueryService; +Once you gain access to this bean, then you can query for the particular state-store that you are interested. See below. +ReadOnlyKeyValueStore<Object, Object> keyValueStore = + interactiveQueryService.getQueryableStoreType("my-store", QueryableStoreTypes.keyValueStore()); +If there are multiple instances of the kafka streams application running, then before you can query them interactively, you need to identify which application instance hosts the key. +InteractiveQueryService API provides methods for identifying the host information. +In order for this to work, you must configure the property application.server as below: +spring.cloud.stream.kafka.streams.binder.configuration.application.server: <server>:<port> +Here are some code snippets: +org.apache.kafka.streams.state.HostInfo hostInfo = interactiveQueryService.getHostInfo("store-name", + key, keySerializer); + +if (interactiveQueryService.getCurrentHostInfo().equals(hostInfo)) { + + //query from the store that is locally available +} +else { + //query from the remote host +} +
    +
    +Accessing the underlying KafkaStreams object +StreamBuilderFactoryBean from spring-kafka that is responsible for constructing the KafkaStreams object can be accessed programmatically. +Each StreamBuilderFactoryBean is registered as stream-builder and appended with the StreamListener method name. +If your StreamListener method is named as process for example, the stream builder bean is named as stream-builder-process. +Since this is a factory bean, it should be accessed by prepending an ampersand (&) when accessing it programmatically. +Following is an example and it assumes the StreamListener method is named as process +StreamsBuilderFactoryBean streamsBuilderFactoryBean = context.getBean("&stream-builder-process", StreamsBuilderFactoryBean.class); + KafkaStreams kafkaStreams = streamsBuilderFactoryBean.getKafkaStreams(); +
    +
    +State Cleanup +By default, the Kafkastreams.cleanup() method is called when the binding is stopped. +See the Spring Kafka documentation. +To modify this behavior simply add a single CleanupConfig @Bean (configured to clean up on start, stop, or neither) to the application context; the bean will be detected and wired into the factory bean. +
    +
    + +RabbitMQ Binder + +
    +Usage +To use the RabbitMQ binder, you can add it to your Spring Cloud Stream application, by using the following Maven coordinates: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-binder-rabbit</artifactId> +</dependency> +Alternatively, you can use the Spring Cloud Stream RabbitMQ Starter, as follows: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-stream-rabbit</artifactId> +</dependency> +
    +
    +RabbitMQ Binder Overview +The following simplified diagram shows how the RabbitMQ binder operates: +
    +RabbitMQ Binder + + + + +rabbit binder + +
    +By default, the RabbitMQ Binder implementation maps each destination to a TopicExchange. +For each consumer group, a Queue is bound to that TopicExchange. +Each consumer instance has a corresponding RabbitMQ Consumer instance for its group’s Queue. +For partitioned producers and consumers, the queues are suffixed with the partition index and use the partition index as the routing key. +For anonymous consumers (those with no group property), an auto-delete queue (with a randomized unique name) is used. +By using the optional autoBindDlq option, you can configure the binder to create and configure dead-letter queues (DLQs) (and a dead-letter exchange DLX, as well as routing infrastructure). +By default, the dead letter queue has the name of the destination, appended with .dlq. +If retry is enabled (maxAttempts > 1), failed messages are delivered to the DLQ after retries are exhausted. +If retry is disabled (maxAttempts = 1), you should set requeueRejected to false (the default) so that failed messages are routed to the DLQ, instead of being re-queued. +In addition, republishToDlq causes the binder to publish a failed message to the DLQ (instead of rejecting it). +This feature lets additional information (such as the stack trace in the x-exception-stacktrace header) be added to the message in headers. +This option does not need retry enabled. +You can republish a failed message after just one attempt. +Starting with version 1.2, you can configure the delivery mode of republished messages. +See the republishDeliveryMode property. + +Setting requeueRejected to true (with republishToDlq=false ) causes the message to be re-queued and redelivered continually, which is likely not what you want unless the reason for the failure is transient. +In general, you should enable retry within the binder by setting maxAttempts to greater than one or by setting republishToDlq to true. + +See for more information about these properties. +The framework does not provide any standard mechanism to consume dead-letter messages (or to re-route them back to the primary queue). +Some options are described in . + +When multiple RabbitMQ binders are used in a Spring Cloud Stream application, it is important to disable 'RabbitAutoConfiguration' to avoid the same configuration from RabbitAutoConfiguration being applied to the two binders. +You can exclude the class by using the @SpringBootApplication annotation. + +Starting with version 2.0, the RabbitMessageChannelBinder sets the RabbitTemplate.userPublisherConnection property to true so that the non-transactional producers avoid deadlocks on consumers, which can happen if cached connections are blocked because of a memory alarm on the broker. + +Currently, a multiplex consumer (a single consumer listening to multiple queues) is only supported for message-driven conssumers; polled consumers can only retrieve messages from a single queue. + +
    +
    +Configuration Options +This section contains settings specific to the RabbitMQ Binder and bound channels. +For general binding configuration options and properties, see the Spring Cloud Stream core documentation. +
    +RabbitMQ Binder Properties +By default, the RabbitMQ binder uses Spring Boot’s ConnectionFactory. +Conseuqently, it supports all Spring Boot configuration options for RabbitMQ. +(For reference, see the Spring Boot documentation). +RabbitMQ configuration options use the spring.rabbitmq prefix. +In addition to Spring Boot options, the RabbitMQ binder supports the following properties: + + +spring.cloud.stream.rabbit.binder.adminAddresses + +A comma-separated list of RabbitMQ management plugin URLs. +Only used when nodes contains more than one entry. +Each entry in this list must have a corresponding entry in spring.rabbitmq.addresses. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See Queue Affinity and the LocalizedQueueConnectionFactory for more information. +Default: empty. + + + +spring.cloud.stream.rabbit.binder.nodes + +A comma-separated list of RabbitMQ node names. +When more than one entry, used to locate the server address where a queue is located. +Each entry in this list must have a corresponding entry in spring.rabbitmq.addresses. +Only needed if you use a RabbitMQ cluster and wish to consume from the node that hosts the queue. +See Queue Affinity and the LocalizedQueueConnectionFactory for more information. +Default: empty. + + + +spring.cloud.stream.rabbit.binder.compressionLevel + +The compression level for compressed bindings. +See java.util.zip.Deflater. +Default: 1 (BEST_LEVEL). + + + +spring.cloud.stream.binder.connection-name-prefix + +A connection name prefix used to name the connection(s) created by this binder. +The name is this prefix followed by #n, where n increments each time a new connection is opened. +Default: none (Spring AMQP default). + + + +
    +
    +RabbitMQ Consumer Properties +The following properties are available for Rabbit consumers only and must be prefixed with spring.cloud.stream.rabbit.bindings.<channelName>.consumer.. + + +acknowledgeMode + +The acknowledge mode. +Default: AUTO. + + + +autoBindDlq + +Whether to automatically declare the DLQ and bind it to the binder DLX. +Default: false. + + + +bindingRoutingKey + +The routing key with which to bind the queue to the exchange (if bindQueue is true). +For partitioned destinations, -<instanceIndex> is appended. +Default: #. + + + +bindQueue + +Whether to bind the queue to the destination exchange. +Set it to false if you have set up your own infrastructure and have previously created and bound the queue. +Default: true. + + + +consumerTagPrefix + +Used to create the consumer tag(s); will be appended by #n where n increments for each consumer created. +Example: ${spring.application.name}-${spring.cloud.stream.bindings.input.group}-${spring.cloud.stream.instance-index}. +Default: none - the broker will generate random consumer tags. + + + +deadLetterQueueName + +The name of the DLQ +Default: prefix+destination.dlq + + + +deadLetterExchange + +A DLX to assign to the queue. +Relevant only if autoBindDlq is true. +Default: 'prefix+DLX' + + + +deadLetterExchangeType + +The type of the DLX to assign to the queue. +Relevant only if autoBindDlq is true. +Default: 'direct' + + + +deadLetterRoutingKey + +A dead letter routing key to assign to the queue. +Relevant only if autoBindDlq is true. +Default: destination + + + +declareDlx + +Whether to declare the dead letter exchange for the destination. +Relevant only if autoBindDlq is true. +Set to false if you have a pre-configured DLX. +Default: true. + + + +declareExchange + +Whether to declare the exchange for the destination. +Default: true. + + + +delayedExchange + +Whether to declare the exchange as a Delayed Message Exchange. +Requires the delayed message exchange plugin on the broker. +The x-delayed-type argument is set to the exchangeType. +Default: false. + + + +dlqDeadLetterExchange + +If a DLQ is declared, a DLX to assign to that queue. +Default: none + + + +dlqDeadLetterRoutingKey + +If a DLQ is declared, a dead letter routing key to assign to that queue. +Default: none + + + +dlqExpires + +How long before an unused dead letter queue is deleted (in milliseconds). +Default: no expiration + + + +dlqLazy + +Declare the dead letter queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Default: false. + + + +dlqMaxLength + +Maximum number of messages in the dead letter queue. +Default: no limit + + + +dlqMaxLengthBytes + +Maximum number of total bytes in the dead letter queue from all messages. +Default: no limit + + + +dlqMaxPriority + +Maximum priority of messages in the dead letter queue (0-255). +Default: none + + + +dlqOverflowBehavior + +Action to take when dlqMaxLength or dlqMaxLengthBytes is exceeded; currently drop-head or reject-publish but refer to the RabbitMQ documentation. +Default: none + + + +dlqTtl + +Default time to live to apply to the dead letter queue when declared (in milliseconds). +Default: no limit + + + +durableSubscription + +Whether the subscription should be durable. +Only effective if group is also set. +Default: true. + + + +exchangeAutoDelete + +If declareExchange is true, whether the exchange should be auto-deleted (that is, removed after the last queue is removed). +Default: true. + + + +exchangeDurable + +If declareExchange is true, whether the exchange should be durable (that is, it survives broker restart). +Default: true. + + + +exchangeType + +The exchange type: direct, fanout or topic for non-partitioned destinations and direct or topic for partitioned destinations. +Default: topic. + + + +exclusive + +Whether to create an exclusive consumer. +Concurrency should be 1 when this is true. +Often used when strict ordering is required but enabling a hot standby instance to take over after a failure. +See recoveryInterval, which controls how often a standby instance attempts to consume. +Default: false. + + + +expires + +How long before an unused queue is deleted (in milliseconds). +Default: no expiration + + + +failedDeclarationRetryInterval + +The interval (in milliseconds) between attempts to consume from a queue if it is missing. +Default: 5000 + + + +headerPatterns + +Patterns for headers to be mapped from inbound messages. +Default: ['*'] (all headers). + + + +lazy + +Declare the queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Default: false. + + + +maxConcurrency + +The maximum number of consumers. +Default: 1. + + + +maxLength + +The maximum number of messages in the queue. +Default: no limit + + + +maxLengthBytes + +The maximum number of total bytes in the queue from all messages. +Default: no limit + + + +maxPriority + +The maximum priority of messages in the queue (0-255). +Default: none + + + +missingQueuesFatal + +When the queue cannot be found, whether to treat the condition as fatal and stop the listener container. +Defaults to false so that the container keeps trying to consume from the queue — for example, when using a cluster and the node hosting a non-HA queue is down. +Default: false + + + +overflowBehavior + +Action to take when maxLength or maxLengthBytes is exceeded; currently drop-head or reject-publish but refer to the RabbitMQ documentation. +Default: none + + + +prefetch + +Prefetch count. +Default: 1. + + + +prefix + +A prefix to be added to the name of the destination and queues. +Default: "". + + + +queueDeclarationRetries + +The number of times to retry consuming from a queue if it is missing. +Relevant only when missingQueuesFatal is true. +Otherwise, the container keeps retrying indefinitely. +Default: 3 + + + +queueNameGroupOnly + +When true, consume from a queue with a name equal to the group. +Otherwise the queue name is destination.group. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. +Default: false. + + + +recoveryInterval + +The interval between connection recovery attempts, in milliseconds. +Default: 5000. + + + +requeueRejected + +Whether delivery failures should be re-queued when retry is disabled or republishToDlq is false. +Default: false. + + + + + +republishDeliveryMode + +When republishToDlq is true, specifies the delivery mode of the republished message. +Default: DeliveryMode.PERSISTENT + + + +republishToDlq + +By default, messages that fail after retries are exhausted are rejected. +If a dead-letter queue (DLQ) is configured, RabbitMQ routes the failed message (unchanged) to the DLQ. +If set to true, the binder republishs failed messages to the DLQ with additional headers, including the exception message and stack trace from the cause of the final failure. +Default: false + + + +transacted + +Whether to use transacted channels. +Default: false. + + + +ttl + +Default time to live to apply to the queue when declared (in milliseconds). +Default: no limit + + + +txSize + +The number of deliveries between acks. +Default: 1. + + + +
    +
    +Advanced Listener Container Configuration +To set listener container properties that are not exposed as binder or binding properties, add a single bean of type ListenerContainerCustomizer to the application context. +The binder and binding properties will be set and then the customizer will be called. +The customizer (configure() method) is provided with the queue name as well as the consumer group as arguments. +
    +
    +Rabbit Producer Properties +The following properties are available for Rabbit producers only and +must be prefixed with spring.cloud.stream.rabbit.bindings.<channelName>.producer.. + + +autoBindDlq + +Whether to automatically declare the DLQ and bind it to the binder DLX. +Default: false. + + + +batchingEnabled + +Whether to enable message batching by producers. +Messages are batched into one message according to the following properties (described in the next three entries in this list): 'batchSize', batchBufferLimit, and batchTimeout. +See Batching for more information. +Default: false. + + + +batchSize + +The number of messages to buffer when batching is enabled. +Default: 100. + + + +batchBufferLimit + +The maximum buffer size when batching is enabled. +Default: 10000. + + + +batchTimeout + +The batch timeout when batching is enabled. +Default: 5000. + + + +bindingRoutingKey + +The routing key with which to bind the queue to the exchange (if bindQueue is true). +Only applies to non-partitioned destinations. +Only applies if requiredGroups are provided and then only to those groups. +Default: #. + + + +bindQueue + +Whether to bind the queue to the destination exchange. +Set it to false if you have set up your own infrastructure and have previously created and bound the queue. +Only applies if requiredGroups are provided and then only to those groups. +Default: true. + + + +compress + +Whether data should be compressed when sent. +Default: false. + + + +deadLetterQueueName + +The name of the DLQ +Only applies if requiredGroups are provided and then only to those groups. +Default: prefix+destination.dlq + + + +deadLetterExchange + +A DLX to assign to the queue. +Relevant only when autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups. +Default: 'prefix+DLX' + + + +deadLetterExchangeType + +The type of the DLX to assign to the queue. +Relevant only if autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups. +Default: 'direct' + + + +deadLetterRoutingKey + +A dead letter routing key to assign to the queue. +Relevant only when autoBindDlq is true. +Applies only when requiredGroups are provided and then only to those groups. +Default: destination + + + +declareDlx + +Whether to declare the dead letter exchange for the destination. +Relevant only if autoBindDlq is true. +Set to false if you have a pre-configured DLX. +Applies only when requiredGroups are provided and then only to those groups. +Default: true. + + + +declareExchange + +Whether to declare the exchange for the destination. +Default: true. + + + +delayExpression + +A SpEL expression to evaluate the delay to apply to the message (x-delay header). +It has no effect if the exchange is not a delayed message exchange. +Default: No x-delay header is set. + + + +delayedExchange + +Whether to declare the exchange as a Delayed Message Exchange. +Requires the delayed message exchange plugin on the broker. +The x-delayed-type argument is set to the exchangeType. +Default: false. + + + +deliveryMode + +The delivery mode. +Default: PERSISTENT. + + + +dlqDeadLetterExchange + +When a DLQ is declared, a DLX to assign to that queue. +Applies only if requiredGroups are provided and then only to those groups. +Default: none + + + +dlqDeadLetterRoutingKey + +When a DLQ is declared, a dead letter routing key to assign to that queue. +Applies only when requiredGroups are provided and then only to those groups. +Default: none + + + +dlqExpires + +How long (in milliseconds) before an unused dead letter queue is deleted. +Applies only when requiredGroups are provided and then only to those groups. +Default: no expiration + + + +dlqLazy + +Declare the dead letter queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when requiredGroups are provided and then only to those groups. + + + +dlqMaxLength + +Maximum number of messages in the dead letter queue. +Applies only if requiredGroups are provided and then only to those groups. +Default: no limit + + + +dlqMaxLengthBytes + +Maximum number of total bytes in the dead letter queue from all messages. +Applies only when requiredGroups are provided and then only to those groups. +Default: no limit + + + +dlqMaxPriority + +Maximum priority of messages in the dead letter queue (0-255) +Applies only when requiredGroups are provided and then only to those groups. +Default: none + + + +dlqTtl + +Default time (in milliseconds) to live to apply to the dead letter queue when declared. +Applies only when requiredGroups are provided and then only to those groups. +Default: no limit + + + +exchangeAutoDelete + +If declareExchange is true, whether the exchange should be auto-delete (it is removed after the last queue is removed). +Default: true. + + + +exchangeDurable + +If declareExchange is true, whether the exchange should be durable (survives broker restart). +Default: true. + + + +exchangeType + +The exchange type: direct, fanout or topic for non-partitioned destinations and direct or topic for partitioned destinations. +Default: topic. + + + +expires + +How long (in milliseconds) before an unused queue is deleted. +Applies only when requiredGroups are provided and then only to those groups. +Default: no expiration + + + +headerPatterns + +Patterns for headers to be mapped to outbound messages. +Default: ['*'] (all headers). + + + +lazy + +Declare the queue with the x-queue-mode=lazy argument. +See Lazy Queues. +Consider using a policy instead of this setting, because using a policy allows changing the setting without deleting the queue. +Applies only when requiredGroups are provided and then only to those groups. +Default: false. + + + +maxLength + +Maximum number of messages in the queue. +Applies only when requiredGroups are provided and then only to those groups. +Default: no limit + + + +maxLengthBytes + +Maximum number of total bytes in the queue from all messages. +Only applies if requiredGroups are provided and then only to those groups. +Default: no limit + + + +maxPriority + +Maximum priority of messages in the queue (0-255). +Only applies if requiredGroups are provided and then only to those groups. +Default: none + + + +prefix + +A prefix to be added to the name of the destination exchange. +Default: "". + + + +queueNameGroupOnly + +When true, consume from a queue with a name equal to the group. +Otherwise the queue name is destination.group. +This is useful, for example, when using Spring Cloud Stream to consume from an existing RabbitMQ queue. +Applies only when requiredGroups are provided and then only to those groups. +Default: false. + + + +routingKeyExpression + +A SpEL expression to determine the routing key to use when publishing messages. +For a fixed routing key, use a literal expression, such as routingKeyExpression='my.routingKey' in a properties file or routingKeyExpression: '''my.routingKey''' in a YAML file. +Default: destination or destination-<partition> for partitioned destinations. + + + +transacted + +Whether to use transacted channels. +Default: false. + + + +ttl + +Default time (in milliseconds) to live to apply to the queue when declared. +Applies only when requiredGroups are provided and then only to those groups. +Default: no limit + + + + +In the case of RabbitMQ, content type headers can be set by external applications. +Spring Cloud Stream supports them as part of an extended internal protocol used for any type of transport — including transports, such as Kafka (prior to 0.11), that do not natively support headers. + +
    +
    +
    +Retry With the RabbitMQ Binder +When retry is enabled within the binder, the listener container thread is suspended for any back off periods that are configured. +This might be important when strict ordering is required with a single consumer. However, for other use cases, it prevents other messages from being processed on that thread. +An alternative to using binder retry is to set up dead lettering with time to live on the dead-letter queue (DLQ) as well as dead-letter configuration on the DLQ itself. +See for more information about the properties discussed here. +You can use the following example configuration to enable this feature: + + +Set autoBindDlq to true. +The binder create a DLQ. +Optionally, you can specify a name in deadLetterQueueName. + + +Set dlqTtl to the back off time you want to wait between redeliveries. + + +Set the dlqDeadLetterExchange to the default exchange. +Expired messages from the DLQ are routed to the original queue, because the default deadLetterRoutingKey is the queue name (destination.group). +Setting to the default exchange is achieved by setting the property with no value, as shown in the next example. + + +To force a message to be dead-lettered, either throw an AmqpRejectAndDontRequeueException or set requeueRejected to true (the default) and throw any exception. +The loop continue without end, which is fine for transient problems, but you may want to give up after some number of attempts. +Fortunately, RabbitMQ provides the x-death header, which lets you determine how many cycles have occurred. +To acknowledge a message after giving up, throw an ImmediateAcknowledgeAmqpException. +
    +Putting it All Together +The following configuration creates an exchange myDestination with queue myDestination.consumerGroup bound to a topic exchange with a wildcard routing key #: +--- +spring.cloud.stream.bindings.input.destination=myDestination +spring.cloud.stream.bindings.input.group=consumerGroup +#disable binder retries +spring.cloud.stream.bindings.input.consumer.max-attempts=1 +#dlx/dlq setup +spring.cloud.stream.rabbit.bindings.input.consumer.auto-bind-dlq=true +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-ttl=5000 +spring.cloud.stream.rabbit.bindings.input.consumer.dlq-dead-letter-exchange= +--- +This configuration creates a DLQ bound to a direct exchange (DLX) with a routing key of myDestination.consumerGroup. +When messages are rejected, they are routed to the DLQ. +After 5 seconds, the message expires and is routed to the original queue by using the queue name as the routing key, as shown in the following example: + +Spring Boot application + +@SpringBootApplication +@EnableBinding(Sink.class) +public class XDeathApplication { + + public static void main(String[] args) { + SpringApplication.run(XDeathApplication.class, args); + } + + @StreamListener(Sink.INPUT) + public void listen(String in, @Header(name = "x-death", required = false) Map<?,?> death) { + if (death != null && death.get("count").equals(3L)) { + // giving up - don't send to DLX + throw new ImmediateAcknowledgeAmqpException("Failed after 4 attempts"); + } + throw new AmqpRejectAndDontRequeueException("failed"); + } + +} + + +Notice that the count property in the x-death header is a Long. +
    +
    +
    +Error Channels +Starting with version 1.3, the binder unconditionally sends exceptions to an error channel for each consumer destination and can also be configured to send async producer send failures to an error channel. +See for more information. +RabbitMQ has two types of send failures: + + +Returned messages, + + +Negatively acknowledged Publisher Confirms. + + +The latter is rare. +According to the RabbitMQ documentation "[A nack] will only be delivered if an internal error occurs in the Erlang process responsible for a queue.". +As well as enabling producer error channels (as described in ), the RabbitMQ binder only sends messages to the channels if the connection factory is appropriately configured, as follows: + + +ccf.setPublisherConfirms(true); + + +ccf.setPublisherReturns(true); + + +When using Spring Boot configuration for the connection factory, set the following properties: + + +spring.rabbitmq.publisher-confirms + + +spring.rabbitmq.publisher-returns + + +The payload of the ErrorMessage for a returned message is a ReturnedAmqpMessageException with the following properties: + + +failedMessage: The spring-messaging Message<?> that failed to be sent. + + +amqpMessage: The raw spring-amqp Message. + + +replyCode: An integer value indicating the reason for the failure (for example, 312 - No route). + + +replyText: A text value indicating the reason for the failure (for example, NO_ROUTE). + + +exchange: The exchange to which the message was published. + + +routingKey: The routing key used when the message was published. + + +For negatively acknowledged confirmations, the payload is a NackedAmqpMessageException with the following properties: + + +failedMessage: The spring-messaging Message<?> that failed to be sent. + + +nackReason: A reason (if available — you may need to examine the broker logs for more information). + + +There is no automatic handling of these exceptions (such as sending to a dead-letter queue). +You can consume these exceptions with your own Spring Integration flow. +
    +
    +Dead-Letter Queue Processing +Because you cannot anticipate how users would want to dispose of dead-lettered messages, the framework does not provide any standard mechanism to handle them. +If the reason for the dead-lettering is transient, you may wish to route the messages back to the original queue. +However, if the problem is a permanent issue, that could cause an infinite loop. +The following Spring Boot application shows an example of how to route those messages back to the original queue but moves them to a third parking lot queue after three attempts. +The second example uses the RabbitMQ Delayed Message Exchange to introduce a delay to the re-queued message. +In this example, the delay increases for each attempt. +These examples use a @RabbitListener to receive messages from the DLQ. +You could also use RabbitTemplate.receive() in a batch process. +The examples assume the original destination is so8400in and the consumer group is so8400. +
    +Non-Partitioned Destinations +The first two examples are for when the destination is not partitioned: +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Hit enter to terminate"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Integer retriesHeader = (Integer) failedMessage.getMessageProperties().getHeaders().get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + failedMessage.getMessageProperties().getHeaders().put(X_RETRIES_HEADER, retriesHeader + 1); + this.rabbitTemplate.send(ORIGINAL_QUEUE, failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + private static final String DELAY_EXCHANGE = "dlqReRouter"; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Hit enter to terminate"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders(); + Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + headers.put(X_RETRIES_HEADER, retriesHeader + 1); + headers.put("x-delay", 5000 * retriesHeader); + this.rabbitTemplate.send(DELAY_EXCHANGE, ORIGINAL_QUEUE, failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public DirectExchange delayExchange() { + DirectExchange exchange = new DirectExchange(DELAY_EXCHANGE); + exchange.setDelayed(true); + return exchange; + } + + @Bean + public Binding bindOriginalToDelay() { + return BindingBuilder.bind(new Queue(ORIGINAL_QUEUE)).to(delayExchange()).with(ORIGINAL_QUEUE); + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +
    +
    +Partitioned Destinations +With partitioned destinations, there is one DLQ for all partitions. We determine the original queue from the headers. +
    +<literal>republishToDlq=false</literal> +When republishToDlq is false, RabbitMQ publishes the message to the DLX/DLQ with an x-death header containing information about the original destination, as shown in the following example: +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_DEATH_HEADER = "x-death"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Hit enter to terminate"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @SuppressWarnings("unchecked") + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders(); + Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + headers.put(X_RETRIES_HEADER, retriesHeader + 1); + List<Map<String, ?>> xDeath = (List<Map<String, ?>>) headers.get(X_DEATH_HEADER); + String exchange = (String) xDeath.get(0).get("exchange"); + List<String> routingKeys = (List<String>) xDeath.get(0).get("routing-keys"); + this.rabbitTemplate.send(exchange, routingKeys.get(0), failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +
    +
    +<literal>republishToDlq=true</literal> +When republishToDlq is true, the republishing recoverer adds the original exchange and routing key to headers, as shown in the following example: +@SpringBootApplication +public class ReRouteDlqApplication { + + private static final String ORIGINAL_QUEUE = "so8400in.so8400"; + + private static final String DLQ = ORIGINAL_QUEUE + ".dlq"; + + private static final String PARKING_LOT = ORIGINAL_QUEUE + ".parkingLot"; + + private static final String X_RETRIES_HEADER = "x-retries"; + + private static final String X_ORIGINAL_EXCHANGE_HEADER = RepublishMessageRecoverer.X_ORIGINAL_EXCHANGE; + + private static final String X_ORIGINAL_ROUTING_KEY_HEADER = RepublishMessageRecoverer.X_ORIGINAL_ROUTING_KEY; + + public static void main(String[] args) throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ReRouteDlqApplication.class, args); + System.out.println("Hit enter to terminate"); + System.in.read(); + context.close(); + } + + @Autowired + private RabbitTemplate rabbitTemplate; + + @RabbitListener(queues = DLQ) + public void rePublish(Message failedMessage) { + Map<String, Object> headers = failedMessage.getMessageProperties().getHeaders(); + Integer retriesHeader = (Integer) headers.get(X_RETRIES_HEADER); + if (retriesHeader == null) { + retriesHeader = Integer.valueOf(0); + } + if (retriesHeader < 3) { + headers.put(X_RETRIES_HEADER, retriesHeader + 1); + String exchange = (String) headers.get(X_ORIGINAL_EXCHANGE_HEADER); + String originalRoutingKey = (String) headers.get(X_ORIGINAL_ROUTING_KEY_HEADER); + this.rabbitTemplate.send(exchange, originalRoutingKey, failedMessage); + } + else { + this.rabbitTemplate.send(PARKING_LOT, failedMessage); + } + } + + @Bean + public Queue parkingLot() { + return new Queue(PARKING_LOT); + } + +} +
    +
    +
    +
    +Partitioning with the RabbitMQ Binder +RabbitMQ does not support partitioning natively. +Sometimes, it is advantageous to send data to specific partitions — for example, when you want to strictly order message processing, all messages for a particular customer should go to the same partition. +The RabbitMessageChannelBinder provides partitioning by binding a queue for each partition to the destination exchange. +The following Java and YAML examples show how to configure the producer: + +Producer + +@SpringBootApplication +@EnableBinding(Source.class) +public class RabbitPartitionProducerApplication { + + private static final Random RANDOM = new Random(System.currentTimeMillis()); + + private static final String[] data = new String[] { + "abc1", "def1", "qux1", + "abc2", "def2", "qux2", + "abc3", "def3", "qux3", + "abc4", "def4", "qux4", + }; + + public static void main(String[] args) { + new SpringApplicationBuilder(RabbitPartitionProducerApplication.class) + .web(false) + .run(args); + } + + @InboundChannelAdapter(channel = Source.OUTPUT, poller = @Poller(fixedRate = "5000")) + public Message<?> generate() { + String value = data[RANDOM.nextInt(data.length)]; + System.out.println("Sending: " + value); + return MessageBuilder.withPayload(value) + .setHeader("partitionKey", value) + .build(); + } + +} + + + +application.yml + + spring: + cloud: + stream: + bindings: + output: + destination: partitioned.destination + producer: + partitioned: true + partition-key-expression: headers['partitionKey'] + partition-count: 2 + required-groups: + - myGroup + + + +The configuration in the prececing example uses the default partitioning (key.hashCode() % partitionCount). +This may or may not provide a suitably balanced algorithm, depending on the key values. +You can override this default by using the partitionSelectorExpression or partitionSelectorClass properties. +The required-groups property is required only if you need the consumer queues to be provisioned when the producer is deployed. +Otherwise, any messages sent to a partition are lost until the corresponding consumer is deployed. + +The following configuration provisions a topic exchange: + + + + + +part exchange + + +The following queues are bound to that exchange: + + + + + +part queues + + +The following bindings associate the queues to the exchange: + + + + + +part bindings + + +The following Java and YAML examples continue the previous examples and show how to configure the consumer: + +Consumer + +@SpringBootApplication +@EnableBinding(Sink.class) +public class RabbitPartitionConsumerApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(RabbitPartitionConsumerApplication.class) + .web(false) + .run(args); + } + + @StreamListener(Sink.INPUT) + public void listen(@Payload String in, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + System.out.println(in + " received from queue " + queue); + } + +} + + + +application.yml + + spring: + cloud: + stream: + bindings: + input: + destination: partitioned.destination + group: myGroup + consumer: + partitioned: true + instance-index: 0 + + + +The RabbitMessageChannelBinder does not support dynamic scaling. +There must be at least one consumer per partition. +The consumer’s instanceIndex is used to indicate which partition is consumed. +Platforms such as Cloud Foundry can have only one instance with an instanceIndex. + +
    +
    +
    + +Spring Cloud Bus + +Spring Cloud Bus links the nodes of a distributed system with a lightweight message +broker. This broker can then be used to broadcast state changes (such as configuration +changes) or other management instructions. A key idea is that the bus is like a +distributed actuator for a Spring Boot application that is scaled out. However, it can +also be used as a communication channel between apps. This project provides starters for +either an AMQP broker or Kafka as the transport. + +Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github. + + + +Quick Start +Spring Cloud Bus works by adding Spring Boot autconfiguration if it detects itself on the +classpath. To enable the bus, add spring-cloud-starter-bus-amqp or +spring-cloud-starter-bus-kafka to your dependency management. Spring Cloud takes care of +the rest. Make sure the broker (RabbitMQ or Kafka) is available and configured. When +running on localhost, you need not do anything. If you run remotely, use Spring Cloud +Connectors or Spring Boot conventions to define the broker credentials, as shown in the +following example for Rabbit: + +application.yml + +spring: + rabbitmq: + host: mybroker.com + port: 5672 + username: user + password: secret + + +The bus currently supports sending messages to all nodes listening or all nodes for a +particular service (as defined by Eureka). The /bus/* actuator namespace has some HTTP +endpoints. Currently, two are implemented. The first, /bus/env, sends key/value pairs to +update each node’s Spring Environment. The second, /bus/refresh, reloads each +application’s configuration, as though they had all been pinged on their /refresh +endpoint. + +The Spring Cloud Bus starters cover Rabbit and Kafka, because those are the two most +common implementations. However, Spring Cloud Stream is quite flexible, and the binder +works with spring-cloud-bus. + + + +Bus Endpoints +Spring Cloud Bus provides two endpoints, /actuator/bus-refresh and /actuator/bus-env +that correspond to individual actuator endpoints in Spring Cloud Commons, +/actuator/refresh and /actuator/env respectively. +
    +Bus Refresh Endpoint +The /actuator/bus-refresh endpoint clears the RefreshScope cache and rebinds +@ConfigurationProperties. See the Refresh Scope documentation for +more information. +To expose the /actuator/bus-refresh endpoint, you need to add following configuration to your +application: +management.endpoints.web.exposure.include=bus-refresh +
    +
    +Bus Env Endpoint +The /actuator/bus-env endpoint updates each instances environment with the specified +key/value pair across multiple instances. +To expose the /actuator/bus-env endpoint, you need to add following configuration to your +application: +management.endpoints.web.exposure.include=bus-env +The /actuator/bus-env endpoint accepts POST requests with the following shape: +{ + "name": "key1", + "value": "value1" +} +
    +
    + +Addressing an Instance +Each instance of the application has a service ID, whose value can be set with +spring.cloud.bus.id and whose value is expected to be a colon-separated list of +identifiers, in order from least specific to most specific. The default value is +constructed from the environment as a combination of the spring.application.name and +server.port (or spring.application.index, if set). The default value of the ID is +constructed in the form of app:index:id, where: + + +app is the vcap.application.name, if it exists, or spring.application.name + + +index is the vcap.application.instance_index, if it exists, +spring.application.index, local.server.port, server.port, or 0 (in that order). + + +id is the vcap.application.instance_id, if it exists, or a random value. + + +The HTTP endpoints accept a destination path parameter, such as +/bus-refresh/customers:9000, where destination is a service ID. If the ID +is owned by an instance on the bus, it processes the message, and all other instances +ignore it. + + +Addressing All Instances of a Service +The destination parameter is used in a Spring PathMatcher (with the path separator +as a colon — :) to determine if an instance processes the message. Using the example +from earlier, /bus-env/customers:** targets all instances of the +customers service regardless of the rest of the service ID. + + +Service ID Must Be Unique +The bus tries twice to eliminate processing an event — once from the original +ApplicationEvent and once from the queue. To do so, it checks the sending service ID +against the current service ID. If multiple instances of a service have the same ID, +events are not processed. When running on a local machine, each service is on a different +port, and that port is part of the ID. Cloud Foundry supplies an index to differentiate. +To ensure that the ID is unique outside Cloud Foundry, set spring.application.index to +something unique for each instance of a service. + + +Customizing the Message Broker +Spring Cloud Bus uses Spring Cloud Stream to +broadcast the messages. So, to get messages to flow, you need only include the binder +implementation of your choice in the classpath. There are convenient starters for the bus +with AMQP (RabbitMQ) and Kafka (spring-cloud-starter-bus-[amqp|kafka]). Generally +speaking, Spring Cloud Stream relies on Spring Boot autoconfiguration conventions for +configuring middleware. For instance, the AMQP broker address can be changed with +spring.rabbitmq.* configuration properties. Spring Cloud Bus has a handful of +native configuration properties in spring.cloud.bus.* (for example, +spring.cloud.bus.destination is the name of the topic to use as the external +middleware). Normally, the defaults suffice. +To learn more about how to customize the message broker settings, consult the Spring Cloud +Stream documentation. + + +Tracing Bus Events +Bus events (subclasses of RemoteApplicationEvent) can be traced by setting +spring.cloud.bus.trace.enabled=true. If you do so, the Spring Boot TraceRepository +(if it is present) shows each event sent and all the acks from each service instance. The +following example comes from the /trace endpoint: +{ + "timestamp": "2015-11-26T10:24:44.411+0000", + "info": { + "signal": "spring.cloud.bus.ack", + "type": "RefreshRemoteApplicationEvent", + "id": "c4d374b7-58ea-4928-a312-31984def293b", + "origin": "stores:8081", + "destination": "*:**" + } + }, + { + "timestamp": "2015-11-26T10:24:41.864+0000", + "info": { + "signal": "spring.cloud.bus.sent", + "type": "RefreshRemoteApplicationEvent", + "id": "c4d374b7-58ea-4928-a312-31984def293b", + "origin": "customers:9000", + "destination": "*:**" + } + }, + { + "timestamp": "2015-11-26T10:24:41.862+0000", + "info": { + "signal": "spring.cloud.bus.ack", + "type": "RefreshRemoteApplicationEvent", + "id": "c4d374b7-58ea-4928-a312-31984def293b", + "origin": "customers:9000", + "destination": "*:**" + } +} +The preceding trace shows that a RefreshRemoteApplicationEvent was sent from +customers:9000, broadcast to all services, and received (acked) by customers:9000 and +stores:8081. +To handle the ack signals yourself, you could add an @EventListener for the +AckRemoteApplicationEvent and SentApplicationEvent types to your app (and enable +tracing). Alternatively, you could tap into the TraceRepository and mine the data from +there. + +Any Bus application can trace acks. However, sometimes, it is +useful to do this in a central service that can do more complex +queries on the data or forward it to a specialized tracing service. + + + +Broadcasting Your Own Events +The Bus can carry any event of type RemoteApplicationEvent. The default transport is +JSON, and the deserializer needs to know which types are going to be used ahead of time. +To register a new type, you must put it in a subpackage of +org.springframework.cloud.bus.event. +To customise the event name, you can use @JsonTypeName on your custom class or rely on +the default strategy, which is to use the simple name of the class. + +Both the producer and the consumer need access to the class definition. + +
    +Registering events in custom packages +If you cannot or do not want to use a subpackage of org.springframework.cloud.bus.event +for your custom events, you must specify which packages to scan for events of type +RemoteApplicationEvent by using the @RemoteApplicationEventScan annotation. Packages +specified with @RemoteApplicationEventScan include subpackages. +For example, consider the following custom event, called MyEvent: +package com.acme; + +public class MyEvent extends RemoteApplicationEvent { + ... +} +You can register that event with the deserializer in the following way: +package com.acme; + +@Configuration +@RemoteApplicationEventScan +public class BusConfiguration { + ... +} +Without specifying a value, the package of the class where @RemoteApplicationEventScan +is used is registered. In this example, com.acme is registered by using the package of +BusConfiguration. +You can also explicitly specify the packages to scan by using the value, basePackages +or basePackageClasses properties on @RemoteApplicationEventScan, as shown in the +following example: +package com.acme; + +@Configuration +//@RemoteApplicationEventScan({"com.acme", "foo.bar"}) +//@RemoteApplicationEventScan(basePackages = {"com.acme", "foo.bar", "fizz.buzz"}) +@RemoteApplicationEventScan(basePackageClasses = BusConfiguration.class) +public class BusConfiguration { + ... +} +All of the preceding examples of @RemoteApplicationEventScan are equivalent, in that the +com.acme package is registered by explicitly specifying the packages on +@RemoteApplicationEventScan. + +You can specify multiple base packages to scan. + +
    +
    +
    + +Spring Cloud Sleuth + +Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant +Greenwich.SR5 + + +Introduction +Spring Cloud Sleuth implements a distributed tracing solution for Spring Cloud. +
    +Terminology +Spring Cloud Sleuth borrows Dapper’s terminology. +Span: The basic unit of work. For example, sending an RPC is a new span, as is sending a response to an RPC. +Spans are identified by a unique 64-bit ID for the span and another 64-bit ID for the trace the span is a part of. +Spans also have other data, such as descriptions, timestamped events, key-value annotations (tags), the ID of the span that caused them, and process IDs (normally IP addresses). +Spans can be started and stopped, and they keep track of their timing information. +Once you create a span, you must stop it at some point in the future. + +The initial span that starts a trace is called a root span. The value of the ID +of that span is equal to the trace ID. + +Trace: A set of spans forming a tree-like structure. +For example, if you run a distributed big-data store, a trace might be formed by a PUT request. +Annotation: Used to record the existence of an event in time. With +Brave instrumentation, we no longer need to set special events +for Zipkin to understand who the client and server are, where +the request started, and where it ended. For learning purposes, +however, we mark these events to highlight what kind +of an action took place. + + +cs: Client Sent. The client has made a request. This annotation indicates the start of the span. + + +sr: Server Received: The server side got the request and started processing it. +Subtracting the cs timestamp from this timestamp reveals the network latency. + + +ss: Server Sent. Annotated upon completion of request processing (when the response got sent back to the client). +Subtracting the sr timestamp from this timestamp reveals the time needed by the server side to process the request. + + +cr: Client Received. Signifies the end of the span. +The client has successfully received the response from the server side. +Subtracting the cs timestamp from this timestamp reveals the whole time needed by the client to receive the response from the server. + + +The following image shows how Span and Trace look in a system, together with the Zipkin annotations: + + + + + +Trace Info propagation + + +Each color of a note signifies a span (there are seven spans - from A to G). +Consider the following note: +Trace Id = X +Span Id = D +Client Sent +This note indicates that the current span has Trace Id set to X and Span Id set to D. +Also, the Client Sent event took place. +The following image shows how parent-child relationships of spans look: + + + + + +Parent child relationship + + +
    +
    +Purpose +The following sections refer to the example shown in the preceding image. +
    +Distributed Tracing with Zipkin +This example has seven spans. +If you go to traces in Zipkin, you can see this number in the second trace, as shown in the following image: + + + + + +Traces + + +However, if you pick a particular trace, you can see four spans, as shown in the following image: + + + + + +Traces Info propagation + + + +When you pick a particular trace, you see merged spans. +That means that, if there were two spans sent to Zipkin with Server Received and Server Sent or Client Received and Client Sent annotations, they are presented as a single span. + +Why is there a difference between the seven and four spans in this case? + + +One span comes from the http:/start span. It has the Server Received (sr) and Server Sent (ss) annotations. + + +Two spans come from the RPC call from service1 to service2 to the http:/foo endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service1 side. +Server Received (sr) and Server Sent (ss) events took place on the service2 side. +These two spans form one logical span related to an RPC call. + + +Two spans come from the RPC call from service2 to service3 to the http:/bar endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service2 side. +The Server Received (sr) and Server Sent (ss) events took place on the service3 side. +These two spans form one logical span related to an RPC call. + + +Two spans come from the RPC call from service2 to service4 to the http:/baz endpoint. +The Client Sent (cs) and Client Received (cr) events took place on the service2 side. +Server Received (sr) and Server Sent (ss) events took place on the service4 side. +These two spans form one logical span related to an RPC call. + + +So, if we count the physical spans, we have one from http:/start, two from service1 calling service2, two from service2 +calling service3, and two from service2 calling service4. In sum, we have a total of seven spans. +Logically, we see the information of four total Spans because we have one span related to the incoming request +to service1 and three spans related to RPC calls. +
    +
    +Visualizing errors +Zipkin lets you visualize errors in your trace. +When an exception was thrown and was not caught, we set proper tags on the span, which Zipkin can then properly colorize. +You could see in the list of traces one trace that is red. That appears because an exception was thrown. +If you click that trace, you see a similar picture, as follows: + + + + + +Error Traces + + +If you then click on one of the spans, you see the following + + + + + +Error Traces Info propagation + + +The span shows the reason for the error and the whole stack trace related to it. +
    +
    +Distributed Tracing with Brave +Starting with version 2.0.0, Spring Cloud Sleuth uses Brave as the tracing library. +Consequently, Sleuth no longer takes care of storing the context but delegates that work to Brave. +Due to the fact that Sleuth had different naming and tagging conventions than Brave, we decided to follow Brave’s conventions from now on. +However, if you want to use the legacy Sleuth approaches, you can set the spring.sleuth.http.legacy.enabled property to true. +
    +
    +Live examples +
    +Click the Pivotal Web Services icon to see it live! + + + + +Zipkin deployed on Pivotal Web Services + +
    +Click here to see it live! +The dependency graph in Zipkin should resemble the following image: + + + + + +Dependencies + + +
    +Click the Pivotal Web Services icon to see it live! + + + + +Zipkin deployed on Pivotal Web Services + +
    +Click here to see it live! +
    +
    +Log correlation +When using grep to read the logs of those four applications by scanning for a trace ID equal to (for example) 2485ec27856c56f4, you get output resembling the following: +service1.log:2016-02-26 11:15:47.561 INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application : Hello from service1. Calling service2 +service2.log:2016-02-26 11:15:47.710 INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application : Hello from service2. Calling service3 and then service4 +service3.log:2016-02-26 11:15:47.895 INFO [service3,2485ec27856c56f4,1210be13194bfe5,true] 68060 --- [nio-8083-exec-1] i.s.c.sleuth.docs.service3.Application : Hello from service3 +service2.log:2016-02-26 11:15:47.924 INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application : Got response from service3 [Hello from service3] +service4.log:2016-02-26 11:15:48.134 INFO [service4,2485ec27856c56f4,1b1845262ffba49d,true] 68061 --- [nio-8084-exec-1] i.s.c.sleuth.docs.service4.Application : Hello from service4 +service2.log:2016-02-26 11:15:48.156 INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application : Got response from service4 [Hello from service4] +service1.log:2016-02-26 11:15:48.182 INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application : Got response from service2 [Hello from service2, response from service3 [Hello from service3] and from service4 [Hello from service4]] +If you use a log aggregating tool (such as Kibana, Splunk, and others), you can order the events that took place. +An example from Kibana would resemble the following image: + + + + + +Log correlation with Kibana + + +If you want to use Logstash, the following listing shows the Grok pattern for Logstash: +filter { + # pattern matching logback pattern + grok { + match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" } + } +} + +If you want to use Grok together with the logs from Cloud Foundry, you have to use the following pattern: + +filter { + # pattern matching logback pattern + grok { + match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" } + } +} +
    +JSON Logback with Logstash +Often, you do not want to store your logs in a text file but in a JSON file that Logstash can immediately pick. +To do so, you have to do the following (for readability, we pass the dependencies in the groupId:artifactId:version notation). +Dependencies Setup + + +Ensure that Logback is on the classpath (ch.qos.logback:logback-core). + + +Add Logstash Logback encode. For example, to use version 4.6, add net.logstash.logback:logstash-logback-encoder:4.6. + + +Logback Setup +Consider the following example of a Logback configuration file (named logback-spring.xml). +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <include resource="org/springframework/boot/logging/logback/defaults.xml"/> + ​ + <springProperty scope="context" name="springAppName" source="spring.application.name"/> + <!-- Example for logging into the build folder of your project --> + <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>​ + + <!-- You can override this to have a custom pattern --> + <property name="CONSOLE_LOG_PATTERN" + value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/> + + <!-- Appender to log to console --> + <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> + <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> + <!-- Minimum logging level to be presented in the console logs--> + <level>DEBUG</level> + </filter> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>utf8</charset> + </encoder> + </appender> + + <!-- Appender to log to file -->​ + <appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>${LOG_FILE}</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern> + <maxHistory>7</maxHistory> + </rollingPolicy> + <encoder> + <pattern>${CONSOLE_LOG_PATTERN}</pattern> + <charset>utf8</charset> + </encoder> + </appender> + ​ + <!-- Appender to log to file in a JSON format --> + <appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>${LOG_FILE}.json</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern> + <maxHistory>7</maxHistory> + </rollingPolicy> + <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> + <providers> + <timestamp> + <timeZone>UTC</timeZone> + </timestamp> + <pattern> + <pattern> + { + "severity": "%level", + "service": "${springAppName:-}", + "trace": "%X{X-B3-TraceId:-}", + "span": "%X{X-B3-SpanId:-}", + "parent": "%X{X-B3-ParentSpanId:-}", + "exportable": "%X{X-Span-Export:-}", + "pid": "${PID:-}", + "thread": "%thread", + "class": "%logger{40}", + "rest": "%message" + } + </pattern> + </pattern> + </providers> + </encoder> + </appender> + ​ + <root level="INFO"> + <appender-ref ref="console"/> + <!-- uncomment this to have also JSON logs --> + <!--<appender-ref ref="logstash"/>--> + <!--<appender-ref ref="flatfile"/>--> + </root> +</configuration> +That Logback configuration file: + + +Logs information from the application in a JSON format to a build/${spring.application.name}.json file. + + +Has commented out two additional appenders: console and standard log file. + + +Has the same logging pattern as the one presented in the previous section. + + + +If you use a custom logback-spring.xml, you must pass the spring.application.name in the bootstrap rather than the application property file. +Otherwise, your custom logback file does not properly read the property. + +
    +
    +
    +Propagating Span Context +The span context is the state that must get propagated to any child spans across process boundaries. +Part of the Span Context is the Baggage. The trace and span IDs are a required part of the span context. +Baggage is an optional part. +Baggage is a set of key:value pairs stored in the span context. +Baggage travels together with the trace and is attached to every span. +Spring Cloud Sleuth understands that a header is baggage-related if the HTTP header is prefixed with baggage- and, for messaging, it starts with baggage_. + +There is currently no limitation of the count or size of baggage items. +However, keep in mind that too many can decrease system throughput or increase RPC latency. +In extreme cases, too much baggage can crash the application, due to exceeding transport-level message or header capacity. + +The following example shows setting baggage on a span: +Span initialSpan = this.tracer.nextSpan().name("span").start(); +ExtraFieldPropagation.set(initialSpan.context(), "foo", "bar"); +ExtraFieldPropagation.set(initialSpan.context(), "UPPER_CASE", "someValue"); +
    +Baggage versus Span Tags +Baggage travels with the trace (every child span contains the baggage of its parent). +Zipkin has no knowledge of baggage and does not receive that information. + +Starting from Sleuth 2.0.0 you have to pass the baggage key names explicitly +in your project configuration. Read more about that setup here + +Tags are attached to a specific span. In other words, they are presented only for that particular span. +However, you can search by tag to find the trace, assuming a span having the searched tag value exists. +If you want to be able to lookup a span based on baggage, you should add a corresponding entry as a tag in the root span. + +The span must be in scope. + +The following listing shows integration tests that use baggage: + +The setup + +spring.sleuth: + baggage-keys: + - baz + - bizarrecase + propagation-keys: + - foo + - upper_case + + + +The code + +initialSpan.tag("foo", + ExtraFieldPropagation.get(initialSpan.context(), "foo")); +initialSpan.tag("UPPER_CASE", + ExtraFieldPropagation.get(initialSpan.context(), "UPPER_CASE")); + + +
    +
    +
    +
    +Adding Sleuth to the Project +This section addresses how to add Sleuth to your project with either Maven or Gradle. + +To ensure that your application name is properly displayed in Zipkin, set the spring.application.name property in bootstrap.yml. + +
    +Only Sleuth (log correlation) +If you want to use only Spring Cloud Sleuth without the Zipkin integration, add the spring-cloud-starter-sleuth module to your project. +The following example shows how to add Sleuth with Maven: + +Maven + +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${release.train.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-sleuth</artifactId> +</dependency> + + + + +We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself. + + +Add the dependency to spring-cloud-starter-sleuth. + + +The following example shows how to add Sleuth with Gradle: + +Gradle + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}" + } +} + +dependencies { + compile "org.springframework.cloud:spring-cloud-starter-sleuth" +} + + + + +We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself. + + +Add the dependency to spring-cloud-starter-sleuth. + + +
    +
    +Sleuth with Zipkin via HTTP +If you want both Sleuth and Zipkin, add the spring-cloud-starter-zipkin dependency. +The following example shows how to do so for Maven: + +Maven + +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${release.train.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-zipkin</artifactId> +</dependency> + + + + +We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself. + + +Add the dependency to spring-cloud-starter-zipkin. + + +The following example shows how to do so for Gradle: + +Gradle + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}" + } +} + +dependencies { + compile "org.springframework.cloud:spring-cloud-starter-zipkin" +} + + + + +We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself. + + +Add the dependency to spring-cloud-starter-zipkin. + + +
    +
    +Sleuth with Zipkin over RabbitMQ or Kafka +If you want to use RabbitMQ or Kafka instead of HTTP, add the spring-rabbit or spring-kafka dependency. +The default destination name is zipkin. +If using Kafka, you must set the property spring.zipkin.sender.type property accordingly: +spring.zipkin.sender.type: kafka + +spring-cloud-sleuth-stream is deprecated and incompatible with these destinations. + +If you want Sleuth over RabbitMQ, add the spring-cloud-starter-zipkin and spring-rabbit +dependencies. +The following example shows how to do so for Gradle: + +Maven + +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${release.train.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-zipkin</artifactId> +</dependency> +<dependency> + <groupId>org.springframework.amqp</groupId> + <artifactId>spring-rabbit</artifactId> +</dependency> + + + + +We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself. + + +Add the dependency to spring-cloud-starter-zipkin. That way, all nested dependencies get downloaded. + + +To automatically configure RabbitMQ, add the spring-rabbit dependency. + + + +Gradle + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${releaseTrainVersion}" + } +} + +dependencies { + compile "org.springframework.cloud:spring-cloud-starter-zipkin" + compile "org.springframework.amqp:spring-rabbit" +} + + + + +We recommend that you add the dependency management through the Spring BOM so that you need not manage versions yourself. + + +Add the dependency to spring-cloud-starter-zipkin. That way, all nested dependencies get downloaded. + + +To automatically configure RabbitMQ, add the spring-rabbit dependency. + + +
    +
    +
    +Overriding the auto-configuration of Zipkin +Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. +In order to get this to work, every tracing system needs to have a Reporter<Span> and Sender. +If you want to override the provided beans you need to give them a specific name. +To do this you can use respectively ZipkinAutoConfiguration.REPORTER_BEAN_NAME and ZipkinAutoConfiguration.SENDER_BEAN_NAME. +@Configuration +protected static class MyConfig { + + @Bean(ZipkinAutoConfiguration.REPORTER_BEAN_NAME) + Reporter<zipkin2.Span> myReporter() { + return AsyncReporter.create(mySender()); + } + + @Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME) + MySender mySender() { + return new MySender(); + } + + static class MySender extends Sender { + + private boolean spanSent = false; + + boolean isSpanSent() { + return this.spanSent; + } + + @Override + public Encoding encoding() { + return Encoding.JSON; + } + + @Override + public int messageMaxBytes() { + return Integer.MAX_VALUE; + } + + @Override + public int messageSizeInBytes(List<byte[]> encodedSpans) { + return encoding().listSizeInBytes(encodedSpans); + } + + @Override + public Call<Void> sendSpans(List<byte[]> encodedSpans) { + this.spanSent = true; + return Call.create(null); + } + + } + +} +
    +
    + +Additional Resources +You can watch a video of Reshmi Krishna and Marcin Grzejszczak talking about Spring Cloud +Sleuth and Zipkin by clicking here. +You can check different setups of Sleuth and Brave in the openzipkin/sleuth-webmvc-example repository. + + +Features + + +Adds trace and span IDs to the Slf4J MDC, so you can extract all the logs from a given trace or span in a log aggregator, as shown in the following example logs: +2016-02-02 15:30:57.902 INFO [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ... +2016-02-02 15:30:58.372 ERROR [bar,6bfd228dc00d216b,6bfd228dc00d216b,false] 23030 --- [nio-8081-exec-3] ... +2016-02-02 15:31:01.936 INFO [bar,46ab0d418373cbc9,46ab0d418373cbc9,false] 23030 --- [nio-8081-exec-4] ... +Notice the [appname,traceId,spanId,exportable] entries from the MDC: + + +spanId: The ID of a specific operation that took place. + + +appname: The name of the application that logged the span. + + +traceId: The ID of the latency graph that contains the span. + + +exportable: Whether the log should be exported to Zipkin. +When would you like the span not to be exportable? +When you want to wrap some operation in a Span and have it written to the logs only. + + + + +Provides an abstraction over common distributed tracing data models: traces, spans (forming a DAG), annotations, and key-value annotations. +Spring Cloud Sleuth is loosely based on HTrace but is compatible with Zipkin (Dapper). + + +Sleuth records timing information to aid in latency analysis. +By using sleuth, you can pinpoint causes of latency in your applications. + + +Sleuth is written to not log too much and to not cause your production application to crash. +To that end, Sleuth: + + +Propagates structural data about your call graph in-band and the rest out-of-band. + + +Includes opinionated instrumentation of layers such as HTTP. + + +Includes a sampling policy to manage volume. + + +Can report to a Zipkin system for query and visualization. + + + + +Instruments common ingress and egress points from Spring applications (servlet filter, async endpoints, rest template, scheduled actions, message channels, Zuul filters, and Feign client). + + +Sleuth includes default logic to join a trace across HTTP or messaging boundaries. +For example, HTTP propagation works over Zipkin-compatible request headers. + + +Sleuth can propagate context (also known as baggage) between processes. +Consequently, if you set a baggage element on a Span, it is sent downstream to other processes over either HTTP or messaging. + + +Provides a way to create or continue spans and add tags and logs through annotations. + + +If spring-cloud-sleuth-zipkin is on the classpath, the app generates and collects Zipkin-compatible traces. +By default, it sends them over HTTP to a Zipkin server on localhost (port 9411). +You can configure the location of the service by setting spring.zipkin.baseUrl. + + +If you depend on spring-rabbit, your app sends traces to a RabbitMQ broker instead of HTTP. + + +If you depend on spring-kafka, and set spring.zipkin.sender.type: kafka, your app sends traces to a Kafka broker instead of HTTP. + + + + + +spring-cloud-sleuth-stream is deprecated and should no longer be used. + + + +Spring Cloud Sleuth is OpenTracing compatible. + + + +If you use Zipkin, configure the probability of spans exported by setting spring.sleuth.sampler.probability +(default: 0.1, which is 10 percent). Otherwise, you might think that Sleuth is not working be cause it omits some spans. + + +The SLF4J MDC is always set and logback users immediately see the trace and span IDs in logs per the example +shown earlier. +Other logging systems have to configure their own formatter to get the same result. +The default is as follows: +logging.pattern.level set to %5p [${spring.zipkin.service.name:${spring.application.name:-}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}] +(this is a Spring Boot feature for logback users). +If you do not use SLF4J, this pattern is NOT automatically applied. + +
    +Introduction to Brave + +Starting with version 2.0.0, Spring Cloud Sleuth uses +Brave as the tracing library. +For your convenience, we embed part of the Brave’s docs here. + + +In the vast majority of cases you need to just use the Tracer +or SpanCustomizer beans from Brave that Sleuth provides. The documentation below contains +a high overview of what Brave is and how it works. + +Brave is a library used to capture and report latency information about distributed operations to Zipkin. +Most users do not use Brave directly. They use libraries or frameworks rather than employ Brave on their behalf. +This module includes a tracer that creates and joins spans that model the latency of potentially distributed work. +It also includes libraries to propagate the trace context over network boundaries (for example, with HTTP headers). +
    +Tracing +Most importantly, you need a brave.Tracer, configured to report to Zipkin. +The following example setup sends trace data (spans) to Zipkin over HTTP (as opposed to Kafka): +class MyClass { + + private final Tracer tracer; + + // Tracer will be autowired + MyClass(Tracer tracer) { + this.tracer = tracer; + } + + void doSth() { + Span span = tracer.newTrace().name("encode").start(); + // ... + } +} + +If your span contains a name longer than 50 chars, then that name is truncated to 50 chars. +Your names have to be explicit and concrete. +Big names lead to latency issues and sometimes even thrown exceptions. + +The tracer creates and joins spans that model the latency of potentially distributed work. +It can employ sampling to reduce overhead during the process, to reduce the amount of data sent to Zipkin, or both. +Spans returned by a tracer report data to Zipkin when finished or do nothing if unsampled. +After starting a span, you can annotate events of interest or add tags containing details or lookup keys. +Spans have a context that includes trace identifiers that place the span at the correct spot in the tree representing the distributed operation. +
    +
    +Local Tracing +When tracing code that never leaves your process, run it inside a scoped span. +@Autowired Tracer tracer; + +// Start a new trace or a span within an existing trace representing an operation +ScopedSpan span = tracer.startScopedSpan("encode"); +try { + // The span is in "scope" meaning downstream code such as loggers can see trace IDs + return encoder.encode(); +} catch (RuntimeException | Error e) { + span.error(e); // Unless you handle exceptions, you might not know the operation failed! + throw e; +} finally { + span.finish(); // always finish the span +} +When you need more features, or finer control, use the Span type: +@Autowired Tracer tracer; + +// Start a new trace or a span within an existing trace representing an operation +Span span = tracer.nextSpan().name("encode").start(); +// Put the span in "scope" so that downstream code such as loggers can see trace IDs +try (SpanInScope ws = tracer.withSpanInScope(span)) { + return encoder.encode(); +} catch (RuntimeException | Error e) { + span.error(e); // Unless you handle exceptions, you might not know the operation failed! + throw e; +} finally { + span.finish(); // note the scope is independent of the span. Always finish a span. +} +Both of the above examples report the exact same span on finish! +In the above example, the span will be either a new root span or the +next child in an existing trace. +
    +
    +Customizing Spans +Once you have a span, you can add tags to it. +The tags can be used as lookup keys or details. +For example, you might add a tag with your runtime version, as shown in the following example: +span.tag("clnt/finagle.version", "6.36.0"); +When exposing the ability to customize spans to third parties, prefer brave.SpanCustomizer as opposed to brave.Span. +The former is simpler to understand and test and does not tempt users with span lifecycle hooks. +interface MyTraceCallback { + void request(Request request, SpanCustomizer customizer); +} +Since brave.Span implements brave.SpanCustomizer, you can pass it to users, as shown in the following example: +for (MyTraceCallback callback : userCallbacks) { + callback.request(request, span); +} +
    +
    +Implicitly Looking up the Current Span +Sometimes, you do not know if a trace is in progress or not, and you do not want users to do null checks. +brave.CurrentSpanCustomizer handles this problem by adding data to any span that’s in progress or drops, as shown in the following example: +Ex. +// The user code can then inject this without a chance of it being null. +@Autowired SpanCustomizer span; + +void userCode() { + span.annotate("tx.started"); + ... +} +
    +
    +RPC tracing + +Check for instrumentation written here and Zipkin’s list before rolling your own RPC instrumentation. + +RPC tracing is often done automatically by interceptors. Behind the scenes, they add tags and events that relate to their role in an RPC operation. +The following example shows how to add a client span: +@Autowired Tracing tracing; +@Autowired Tracer tracer; + +// before you send a request, add metadata that describes the operation +span = tracer.nextSpan().name(service + "/" + method).kind(CLIENT); +span.tag("myrpc.version", "1.0.0"); +span.remoteServiceName("backend"); +span.remoteIpAndPort("172.3.4.1", 8108); + +// Add the trace context to the request, so it can be propagated in-band +tracing.propagation().injector(Request::addHeader) + .inject(span.context(), request); + +// when the request is scheduled, start the span +span.start(); + +// if there is an error, tag the span +span.tag("error", error.getCode()); +// or if there is an exception +span.error(exception); + +// when the response is complete, finish the span +span.finish(); +
    +One-Way tracing +Sometimes, you need to model an asynchronous operation where there is a +request but no response. In normal RPC tracing, you use span.finish() +to indicate that the response was received. In one-way tracing, you use +span.flush() instead, as you do not expect a response. +The following example shows how a client might model a one-way operation: +@Autowired Tracing tracing; +@Autowired Tracer tracer; + +// start a new span representing a client request +oneWaySend = tracer.nextSpan().name(service + "/" + method).kind(CLIENT); + +// Add the trace context to the request, so it can be propagated in-band +tracing.propagation().injector(Request::addHeader) + .inject(oneWaySend.context(), request); + +// fire off the request asynchronously, totally dropping any response +request.execute(); + +// start the client side and flush instead of finish +oneWaySend.start().flush(); +The following example shows how a server might handle a one-way operation: +@Autowired Tracing tracing; +@Autowired Tracer tracer; + +// pull the context out of the incoming request +extractor = tracing.propagation().extractor(Request::getHeader); + +// convert that context to a span which you can name and add tags to +oneWayReceive = nextSpan(tracer, extractor.extract(request)) + .name("process-request") + .kind(SERVER) + ... add tags etc. + +// start the server side and flush instead of finish +oneWayReceive.start().flush(); + +// you should not modify this span anymore as it is complete. However, +// you can create children to represent follow-up work. +next = tracer.newSpan(oneWayReceive.context()).name("step2").start(); +
    +
    +
    +
    + +Sampling +Sampling may be employed to reduce the data collected and reported out of process. +When a span is not sampled, it adds no overhead (a noop). +Sampling is an up-front decision, meaning that the decision to report data is made at the first operation in a trace and that decision is propagated downstream. +By default, a global sampler applies a single rate to all traced operations. +Tracer.Builder.sampler controls this setting, and it defaults to tracing every request. +
    +Declarative sampling +Some applications need to sample based on the type or annotations of a java method. +Most users use a framework interceptor to automate this sort of policy. +The following example shows how that might work internally: +@Autowired Tracer tracer; + +// derives a sample rate from an annotation on a java method +DeclarativeSampler<Traced> sampler = DeclarativeSampler.create(Traced::sampleRate); + +@Around("@annotation(traced)") +public Object traceThing(ProceedingJoinPoint pjp, Traced traced) throws Throwable { + // When there is no trace in progress, this decides using an annotation + Sampler decideUsingAnnotation = declarativeSampler.toSampler(traced); + Tracer tracer = tracer.withSampler(decideUsingAnnotation); + + // This code looks the same as if there was no declarative override + ScopedSpan span = tracer.startScopedSpan(spanName(pjp)); + try { + return pjp.proceed(); + } catch (RuntimeException | Error e) { + span.error(e); + throw e; + } finally { + span.finish(); + } +} +
    +
    +Custom sampling +Depending on what the operation is, you may want to apply different policies. +For example, you might not want to trace requests to static resources such as images, or you might want to trace all requests to a new api. +Most users use a framework interceptor to automate this sort of policy. +The following example shows how that might work internally: +@Autowired Tracer tracer; +@Autowired Sampler fallback; + +Span nextSpan(final Request input) { + Sampler requestBased = Sampler() { + @Override public boolean isSampled(long traceId) { + if (input.url().startsWith("/experimental")) { + return true; + } else if (input.url().startsWith("/static")) { + return false; + } + return fallback.isSampled(traceId); + } + }; + return tracer.withSampler(requestBased).nextSpan(); +} +
    +
    +Sampling in Spring Cloud Sleuth +By default Spring Cloud Sleuth sets all spans to non-exportable. +That means that traces appear in logs but not in any remote store. +For testing the default is often enough, and it probably is all you need if you use only the logs (for example, with an ELK aggregator). +If you export span data to Zipkin, there is also an Sampler.ALWAYS_SAMPLE setting that exports everything and a ProbabilityBasedSampler setting that samples a fixed fraction of spans. + +The ProbabilityBasedSampler is the default if you use spring-cloud-sleuth-zipkin. +You can configure the exports by setting spring.sleuth.sampler.probability. +The passed value needs to be a double from 0.0 to 1.0. + +A sampler can be installed by creating a bean definition, as shown in the following example: +@Bean +public Sampler defaultSampler() { + return Sampler.ALWAYS_SAMPLE; +} + +You can set the HTTP header X-B3-Flags to 1, or, when doing messaging, you can set the spanFlags header to 1. +Doing so forces the current span to be exportable regardless of the sampling decision. + +In order to use the rate-limited sampler set the spring.sleuth.sampler.rate property to choose an amount of traces to accept on a per-second interval. The minimum number is 0 and the max is 2,147,483,647 (max int). +
    +
    + +Propagation +Propagation is needed to ensure activities originating from the same root are collected together in the same trace. +The most common propagation approach is to copy a trace context from a client by sending an RPC request to a server receiving it. +For example, when a downstream HTTP call is made, its trace context is encoded as request headers and sent along with it, as shown in the following image: + Client Span Server Span +┌──────────────────┐ ┌──────────────────┐ +│ │ │ │ +│ TraceContext │ Http Request Headers │ TraceContext │ +│ ┌──────────────┐ │ ┌───────────────────┐ │ ┌──────────────┐ │ +│ │ TraceId │ │ │ X─B3─TraceId │ │ │ TraceId │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ ParentSpanId │ │ Extract │ X─B3─ParentSpanId │ Inject │ │ ParentSpanId │ │ +│ │ ├─┼─────────>│ ├────────┼>│ │ │ +│ │ SpanId │ │ │ X─B3─SpanId │ │ │ SpanId │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ Sampled │ │ │ X─B3─Sampled │ │ │ Sampled │ │ +│ └──────────────┘ │ └───────────────────┘ │ └──────────────┘ │ +│ │ │ │ +└──────────────────┘ └──────────────────┘ +The names above are from B3 Propagation, which is built-in to Brave and has implementations in many languages and frameworks. +Most users use a framework interceptor to automate propagation. +The next two examples show how that might work for a client and a server. +The following example shows how client-side propagation might work: +@Autowired Tracing tracing; + +// configure a function that injects a trace context into a request +injector = tracing.propagation().injector(Request.Builder::addHeader); + +// before a request is sent, add the current span's context to it +injector.inject(span.context(), request); +The following example shows how server-side propagation might work: +@Autowired Tracing tracing; +@Autowired Tracer tracer; + +// configure a function that extracts the trace context from a request +extractor = tracing.propagation().extractor(Request::getHeader); + +// when a server receives a request, it joins or starts a new trace +span = tracer.nextSpan(extractor.extract(request)); +
    +Propagating extra fields +Sometimes you need to propagate extra fields, such as a request ID or an alternate trace context. +For example, if you are in a Cloud Foundry environment, you might want to pass the request ID, as shown in the following example: +// when you initialize the builder, define the extra field you want to propagate +Tracing.newBuilder().propagationFactory( + ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-vcap-request-id") +); + +// later, you can tag that request ID or use it in log correlation +requestId = ExtraFieldPropagation.get("x-vcap-request-id"); +You may also need to propagate a trace context that you are not using. +For example, you may be in an Amazon Web Services environment but not be reporting data to X-Ray. +To ensure X-Ray can co-exist correctly, pass-through its tracing header, as shown in the following example: +tracingBuilder.propagationFactory( + ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "x-amzn-trace-id") +); + +In Spring Cloud Sleuth all elements of the tracing builder Tracing.newBuilder() +are defined as beans. So if you want to pass a custom PropagationFactory, it’s enough +for you to create a bean of that type and we will set it in the Tracing bean. + +
    +Prefixed fields +If they follow a common pattern, you can also prefix fields. +The following example shows how to propagate x-vcap-request-id the field as-is but send the country-code and user-id fields on the wire as x-baggage-country-code and x-baggage-user-id, respectively: +Tracing.newBuilder().propagationFactory( + ExtraFieldPropagation.newFactoryBuilder(B3Propagation.FACTORY) + .addField("x-vcap-request-id") + .addPrefixedFields("x-baggage-", Arrays.asList("country-code", "user-id")) + .build() +); +Later, you can call the following code to affect the country code of the current trace context: +ExtraFieldPropagation.set("x-country-code", "FO"); +String countryCode = ExtraFieldPropagation.get("x-country-code"); +Alternatively, if you have a reference to a trace context, you can use it explicitly, as shown in the following example: +ExtraFieldPropagation.set(span.context(), "x-country-code", "FO"); +String countryCode = ExtraFieldPropagation.get(span.context(), "x-country-code"); + +A difference from previous versions of Sleuth is that, with Brave, you must pass the list of baggage keys. +There are two properties to achieve this. +With the spring.sleuth.baggage-keys, you set keys that get prefixed with baggage- for HTTP calls and baggage_ for messaging. +You can also use the spring.sleuth.propagation-keys property to pass a list of prefixed keys that are whitelisted without any prefix. +Notice that there’s no x- in front of the header keys. + +In order to automatically set the baggage values to Slf4j’s MDC, you have to set +the spring.sleuth.log.slf4j.whitelisted-mdc-keys property with a list of whitelisted +baggage and propagation keys. E.g. spring.sleuth.log.slf4j.whitelisted-mdc-keys=foo will set the value of the foo baggage into MDC. + +Remember that adding entries to MDC can drastically decrease the performance of your application! + +If you want to add the baggage entries as tags, to make it possible to search for spans via the baggage entries, you can set the value of +spring.sleuth.propagation.tag.whitelisted-keys with a list of whitelisted baggage keys. To disable the feature you have to pass the spring.sleuth.propagation.tag.enabled=false property. +
    +
    +Extracting a Propagated Context +The TraceContext.Extractor<C> reads trace identifiers and sampling status from an incoming request or message. +The carrier is usually a request object or headers. +This utility is used in standard instrumentation (such as HttpServerHandler) but can also be used for custom RPC or messaging code. +TraceContextOrSamplingFlags is usually used only with Tracer.nextSpan(extracted), unless you are +sharing span IDs between a client and a server. +
    +
    +Sharing span IDs between Client and Server +A normal instrumentation pattern is to create a span representing the server side of an RPC. +Extractor.extract might return a complete trace context when applied to an incoming client request. +Tracer.joinSpan attempts to continue this trace, using the same span ID if supported or creating a child span +if not. When the span ID is shared, the reported data includes a flag saying so. +The following image shows an example of B3 propagation: + ┌───────────────────┐ ┌───────────────────┐ + Incoming Headers │ TraceContext │ │ TraceContext │ +┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │ +│ X─B3-TraceId │─────────┼─┼> TraceId │ │──────┼─┼> TraceId │ │ +│ │ │ │ │ │ │ │ │ │ +│ X─B3-ParentSpanId │─────────┼─┼> ParentSpanId │ │──────┼─┼> ParentSpanId │ │ +│ │ │ │ │ │ │ │ │ │ +│ X─B3-SpanId │─────────┼─┼> SpanId │ │──────┼─┼> SpanId │ │ +└───────────────────┘ │ │ │ │ │ │ │ │ + │ │ │ │ │ │ Shared: true │ │ + │ └───────────────┘ │ │ └───────────────┘ │ + └───────────────────┘ └───────────────────┘ +Some propagation systems forward only the parent span ID, detected when Propagation.Factory.supportsJoin() == false. +In this case, a new span ID is always provisioned, and the incoming context determines the parent ID. +The following image shows an example of AWS propagation: + ┌───────────────────┐ ┌───────────────────┐ + x-amzn-trace-id │ TraceContext │ │ TraceContext │ +┌───────────────────┐(extract)│ ┌───────────────┐ │(join)│ ┌───────────────┐ │ +│ Root │─────────┼─┼> TraceId │ │──────┼─┼> TraceId │ │ +│ │ │ │ │ │ │ │ │ │ +│ Parent │─────────┼─┼> SpanId │ │──────┼─┼> ParentSpanId │ │ +└───────────────────┘ │ └───────────────┘ │ │ │ │ │ + └───────────────────┘ │ │ SpanId: New │ │ + │ └───────────────┘ │ + └───────────────────┘ +Note: Some span reporters do not support sharing span IDs. +For example, if you set Tracing.Builder.spanReporter(amazonXrayOrGoogleStackdrive), you should disable join by setting Tracing.Builder.supportsJoin(false). +Doing so forces a new child span on Tracer.joinSpan(). +
    +
    +Implementing Propagation +TraceContext.Extractor<C> is implemented by a Propagation.Factory plugin. +Internally, this code creates the union type, TraceContextOrSamplingFlags, with one of the following: +* TraceContext if trace and span IDs were present. +* TraceIdContext if a trace ID was present but span IDs were not present. +* SamplingFlags if no identifiers were present. +Some Propagation implementations carry extra data from the point of extraction (for example, reading incoming headers) to injection (for example, writing outgoing headers). +For example, it might carry a request ID. +When implementations have extra data, they handle it as follows: +* If a TraceContext were extracted, add the extra data as TraceContext.extra(). +* Otherwise, add it as TraceContextOrSamplingFlags.extra(), which Tracer.nextSpan handles. +
    +
    +
    + +Current Tracing Component +Brave supports a current tracing component concept, which should only be used when you have no other way to get a reference. +This was made for JDBC connections, as they often initialize prior to the tracing component. +The most recent tracing component instantiated is available through Tracing.current(). +You can also use Tracing.currentTracer() to get only the tracer. +If you use either of these methods, do not cache the result. +Instead, look them up each time you need them. + + +Current Span +Brave supports a current span concept which represents the in-flight operation. +You can use Tracer.currentSpan() to add custom tags to a span and Tracer.nextSpan() to create a child of whatever is in-flight. + +In Sleuth, you can autowire the Tracer bean to retrieve the current span via +tracer.currentSpan() method. To retrieve the current context just call +tracer.currentSpan().context(). To get the current trace id as String +you can use the traceIdString() method like this: tracer.currentSpan().context().traceIdString(). + +
    +Setting a span in scope manually +When writing new instrumentation, it is important to place a span you created in scope as the current span. +Not only does doing so let users access it with Tracer.currentSpan(), but it also allows customizations such as SLF4J MDC to see the current trace IDs. +Tracer.withSpanInScope(Span) facilitates this and is most conveniently employed by using the try-with-resources idiom. +Whenever external code might be invoked (such as proceeding an interceptor or otherwise), place the span in scope, as shown in the following example: +@Autowired Tracer tracer; + +try (SpanInScope ws = tracer.withSpanInScope(span)) { + return inboundRequest.invoke(); +} finally { // note the scope is independent of the span + span.finish(); +} +In edge cases, you may need to clear the current span temporarily (for example, launching a task that should not be associated with the current request). To do tso, pass null to withSpanInScope, as shown in the following example: +@Autowired Tracer tracer; + +try (SpanInScope cleared = tracer.withSpanInScope(null)) { + startBackgroundThread(); +} +
    +
    + +Instrumentation +Spring Cloud Sleuth automatically instruments all your Spring applications, so you should not have to do anything to activate it. +The instrumentation is added by using a variety of technologies according to the stack that is available. For example, for a servlet web application, we use a Filter, and, for Spring Integration, we use ChannelInterceptors. +You can customize the keys used in span tags. +To limit the volume of span data, an HTTP request is, by default, tagged only with a handful of metadata, such as the status code, the host, and the URL. +You can add request headers by configuring spring.sleuth.keys.http.headers (a list of header names). + +Tags are collected and exported only if there is a Sampler that allows it. By default, there is no such Sampler, to ensure that there is no danger of accidentally collecting too much data without configuring something). + + + +Span lifecycle +You can do the following operations on the Span by means of brave.Tracer: + + +start: When you start a span, its name is assigned and the start timestamp is recorded. + + +close: The span gets finished (the end time of the span is recorded) and, if the span is sampled, it is eligible for collection (for example, to Zipkin). + + +continue: A new instance of span is created. +It is a copy of the one that it continues. + + +detach: The span does not get stopped or closed. +It only gets removed from the current thread. + + +create with explicit parent: You can create a new span and set an explicit parent for it. + + + +Spring Cloud Sleuth creates an instance of Tracer for you. In order to use it, you can autowire it. + +
    +Creating and finishing spans +You can manually create spans by using the Tracer, as shown in the following example: +// Start a span. If there was a span present in this thread it will become +// the `newSpan`'s parent. +Span newSpan = this.tracer.nextSpan().name("calculateTax"); +try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(newSpan.start())) { + // ... + // You can tag a span + newSpan.tag("taxValue", taxValue); + // ... + // You can log an event on a span + newSpan.annotate("taxCalculated"); +} +finally { + // Once done remember to finish the span. This will allow collecting + // the span to send it to Zipkin + newSpan.finish(); +} +In the preceding example, we could see how to create a new instance of the span. +If there is already a span in this thread, it becomes the parent of the new span. + +Always clean after you create a span. Also, always finish any span that you want to send to Zipkin. + + +If your span contains a name greater than 50 chars, that name is truncated to 50 chars. +Your names have to be explicit and concrete. Big names lead to latency issues and sometimes even exceptions. + +
    +
    +Continuing Spans +Sometimes, you do not want to create a new span but you want to continue one. An example of such a +situation might be as follows: + + +AOP: If there was already a span created before an aspect was reached, you might not want to create a new span. + + +Hystrix: Executing a Hystrix command is most likely a logical part of the current processing. +It is in fact merely a technical implementation detail that you would not necessarily want to reflect in tracing as a separate being. + + +To continue a span, you can use brave.Tracer, as shown in the following example: +// let's assume that we're in a thread Y and we've received +// the `initialSpan` from thread X +Span continuedSpan = this.tracer.toSpan(newSpan.context()); +try { + // ... + // You can tag a span + continuedSpan.tag("taxValue", taxValue); + // ... + // You can log an event on a span + continuedSpan.annotate("taxCalculated"); +} +finally { + // Once done remember to flush the span. That means that + // it will get reported but the span itself is not yet finished + continuedSpan.flush(); +} +
    +
    +Creating a Span with an explicit Parent +You might want to start a new span and provide an explicit parent of that span. +Assume that the parent of a span is in one thread and you want to start a new span in another thread. +In Brave, whenever you call nextSpan(), it creates a span in reference to the span that is currently in scope. +You can put the span in scope and then call nextSpan(), as shown in the following example: +// let's assume that we're in a thread Y and we've received +// the `initialSpan` from thread X. `initialSpan` will be the parent +// of the `newSpan` +Span newSpan = null; +try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(initialSpan)) { + newSpan = this.tracer.nextSpan().name("calculateCommission"); + // ... + // You can tag a span + newSpan.tag("commissionValue", commissionValue); + // ... + // You can log an event on a span + newSpan.annotate("commissionCalculated"); +} +finally { + // Once done remember to finish the span. This will allow collecting + // the span to send it to Zipkin. The tags and events set on the + // newSpan will not be present on the parent + if (newSpan != null) { + newSpan.finish(); + } +} + +After creating such a span, you must finish it. Otherwise it is not reported (for example, to Zipkin). + +
    +
    + +Naming spans +Picking a span name is not a trivial task. A span name should depict an operation name. +The name should be low cardinality, so it should not include identifiers. +Since there is a lot of instrumentation going on, some span names are artificial: + + +controller-method-name when received by a Controller with a method name of controllerMethodName + + +async for asynchronous operations done with wrapped Callable and Runnable interfaces. + + +Methods annotated with @Scheduled return the simple name of the class. + + +Fortunately, for asynchronous processing, you can provide explicit naming. +
    +<literal>@SpanName</literal> Annotation +You can name the span explicitly by using the @SpanName annotation, as shown in the following example: + @SpanName("calculateTax") + class TaxCountingRunnable implements Runnable { + + @Override + public void run() { + // perform logic + } + + } + +} +In this case, when processed in the following manner, the span is named calculateTax: +Runnable runnable = new TraceRunnable(this.tracing, spanNamer, + new TaxCountingRunnable()); +Future<?> future = executorService.submit(runnable); +// ... some additional logic ... +future.get(); +
    +
    +<literal>toString()</literal> method +It is pretty rare to create separate classes for Runnable or Callable. +Typically, one creates an anonymous instance of those classes. +You cannot annotate such classes. +To overcome that limitation, if there is no @SpanName annotation present, we check whether the class has a custom implementation of the toString() method. +Running such code leads to creating a span named calculateTax, as shown in the following example: +Runnable runnable = new TraceRunnable(this.tracing, spanNamer, new Runnable() { + @Override + public void run() { + // perform logic + } + + @Override + public String toString() { + return "calculateTax"; + } +}); +Future<?> future = executorService.submit(runnable); +// ... some additional logic ... +future.get(); +
    +
    + +Managing Spans with Annotations +You can manage spans with a variety of annotations. +
    +Rationale +There are a number of good reasons to manage spans with annotations, including: + + +API-agnostic means to collaborate with a span. Use of annotations lets users add to a span with no library dependency on a span api. +Doing so lets Sleuth change its core API to create less impact to user code. + + +Reduced surface area for basic span operations. Without this feature, you must use the span api, which has lifecycle commands that could be used incorrectly. +By only exposing scope, tag, and log functionality, you can collaborate without accidentally breaking span lifecycle. + + +Collaboration with runtime generated code. With libraries such as Spring Data and Feign, the implementations of interfaces are generated at runtime. +Consequently, span wrapping of objects was tedious. +Now you can provide annotations over interfaces and the arguments of those interfaces. + + +
    +
    +Creating New Spans +If you do not want to create local spans manually, you can use the @NewSpan annotation. +Also, we provide the @SpanTag annotation to add tags in an automated fashion. +Now we can consider some examples of usage. +@NewSpan +void testMethod(); +Annotating the method without any parameter leads to creating a new span whose name equals the annotated method name. +@NewSpan("customNameOnTestMethod4") +void testMethod4(); +If you provide the value in the annotation (either directly or by setting the name parameter), the created span has the provided value as the name. +// method declaration +@NewSpan(name = "customNameOnTestMethod5") +void testMethod5(@SpanTag("testTag") String param); + +// and method execution +this.testBean.testMethod5("test"); +You can combine both the name and a tag. Let’s focus on the latter. +In this case, the value of the annotated method’s parameter runtime value becomes the value of the tag. +In our sample, the tag key is testTag, and the tag value is test. +@NewSpan(name = "customNameOnTestMethod3") +@Override +public void testMethod3() { +} +You can place the @NewSpan annotation on both the class and an interface. +If you override the interface’s method and provide a different value for the @NewSpan annotation, the most +concrete one wins (in this case customNameOnTestMethod3 is set). +
    +
    +Continuing Spans +If you want to add tags and annotations to an existing span, you can use the @ContinueSpan annotation, as shown in the following example: +// method declaration +@ContinueSpan(log = "testMethod11") +void testMethod11(@SpanTag("testTag11") String param); + +// method execution +this.testBean.testMethod11("test"); +this.testBean.testMethod13(); +(Note that, in contrast with the @NewSpan annotation ,you can also add logs with the log parameter.) +That way, the span gets continued and: + + +Log entries named testMethod11.before and testMethod11.after are created. + + +If an exception is thrown, a log entry named testMethod11.afterFailure is also created. + + +A tag with a key of testTag11 and a value of test is created. + + +
    +
    +Advanced Tag Setting +There are 3 different ways to add tags to a span. All of them are controlled by the SpanTag annotation. +The precedence is as follows: + + +Try with a bean of TagValueResolver type and a provided name. + + +If the bean name has not been provided, try to evaluate an expression. +We search for a TagValueExpressionResolver bean. +The default implementation uses SPEL expression resolution. +IMPORTANT You can only reference properties from the SPEL expression. Method execution is not allowed due to security constraints. + + +If we do not find any expression to evaluate, return the toString() value of the parameter. + + +
    +Custom extractor +The value of the tag for the following method is computed by an implementation of TagValueResolver interface. +Its class name has to be passed as the value of the resolver attribute. +Consider the following annotated method: +@NewSpan +public void getAnnotationForTagValueResolver( + @SpanTag(key = "test", resolver = TagValueResolver.class) String test) { +} +Now further consider the following TagValueResolver bean implementation: +@Bean(name = "myCustomTagValueResolver") +public TagValueResolver tagValueResolver() { + return parameter -> "Value from myCustomTagValueResolver"; +} +The two preceding examples lead to setting a tag value equal to Value from myCustomTagValueResolver. +
    +
    +Resolving Expressions for a Value +Consider the following annotated method: +@NewSpan +public void getAnnotationForTagValueExpression( + @SpanTag(key = "test", expression = "'hello' + ' characters'") String test) { +} +No custom implementation of a TagValueExpressionResolver leads to evaluation of the SPEL expression, and a tag with a value of 4 characters is set on the span. +If you want to use some other expression resolution mechanism, you can create your own implementation of the bean. +
    +
    +Using the <literal>toString()</literal> method +Consider the following annotated method: +@NewSpan +public void getAnnotationForArgumentToString(@SpanTag("test") Long param) { +} +Running the preceding method with a value of 15 leads to setting a tag with a String value of "15". +
    +
    +
    + +Customizations +
    +Customizers +With Brave 5.7 you have various options of providing customizers for your project. Brave ships with + + +TracingCustomizer - allows configuration plugins to collaborate on building an instance of Tracing. + + +CurrentTraceContextCustomizer - allows configuration plugins to collaborate on building an instance of CurrentTraceContext. + + +ExtraFieldCustomizer - allows configuration plugins to collaborate on building an instance of ExtraFieldPropagation.Factory. + + +Sleuth will search for beans of those types and automatically apply customizations. +
    +
    +HTTP +If a customization of client / server parsing of the HTTP related spans is +required, just register a bean of type brave.http.HttpClientParser or +brave.http.HttpServerParser. If client /server sampling is required, just +register a bean of type brave.sampler.SamplerFunction<HttpRequest> and name +the bean sleuthHttpClientSampler for client sampler and +sleuthHttpServerSampler for server sampler. +For your convenience the @HttpClientSampler and @HttpServerSampler +annotations can be used to inject the proper beans or to reference the bean +names via their static String NAME fields. +Check out Brave’s code to see an example of how to make a path-based sampler +https://github.com/openzipkin/brave/tree/master/instrumentation/http#sampling-policy +If you want to completely rewrite the HttpTracing bean you can use the SkipPatternProvider +interface to retrieve the URL Pattern for spans that should be not sampled. Below you can see +an example of usage of SkipPatternProvider inside a server side, Sampler<HttpRequest>. +@Configuration +class Config { + @Bean(name = HttpServerSampler.NAME) + SamplerFunction<HttpRequest> myHttpSampler(SkipPatternProvider provider) { + Pattern pattern = provider.skipPattern(); + return request -> { + String url = request.path(); + boolean shouldSkip = pattern.matcher(url).matches(); + if (shouldSkip) { + return false; + } + return null; + }; + } +} +
    +
    +<literal>TracingFilter</literal> +You can also modify the behavior of the TracingFilter, which is the component that is responsible for processing the input HTTP request and adding tags basing on the HTTP response. +You can customize the tags or modify the response headers by registering your own instance of the TracingFilter bean. +In the following example, we register the TracingFilter bean, add the ZIPKIN-TRACE-ID response header containing the current Span’s trace id, and add a tag with key custom and a value tag to the span. +@Component +@Order(TraceWebServletAutoConfiguration.TRACING_FILTER_ORDER + 1) +class MyFilter extends GenericFilterBean { + + private final Tracer tracer; + + MyFilter(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + Span currentSpan = this.tracer.currentSpan(); + if (currentSpan == null) { + chain.doFilter(request, response); + return; + } + // for readability we're returning trace id in a hex form + ((HttpServletResponse) response).addHeader("ZIPKIN-TRACE-ID", + currentSpan.context().traceIdString()); + // we can also add some custom tags + currentSpan.tag("custom", "tag"); + chain.doFilter(request, response); + } + +} +
    +
    +RPC +Sleuth automatically configures the RpcTracing bean which serves as a +foundation for RPC instrumentation such as gRPC or Dubbo. +If a customization of client / server sampling of the RPC traces is required, +just register a bean of type brave.sampler.SamplerFunction<RpcRequest> and +name the bean sleuthRpcClientSampler for client sampler and +sleuthRpcServerSampler for server sampler. +For your convenience the @RpcClientSampler and @RpcServerSampler +annotations can be used to inject the proper beans or to reference the bean +names via their static String NAME fields. +Ex. Here’s a sampler that traces 100 "GetUserToken" server requests per second. +This doesn’t start new traces for requests to the health check service. Other +requests will use the global sampling configuration. +@Configuration +class Config { + @Bean(name = RpcServerSampler.NAME) + SamplerFunction<RpcRequest> myRpcSampler() { + Matcher<RpcRequest> userAuth = and(serviceEquals("users.UserService"), + methodEquals("GetUserToken")); + return RpcRuleSampler.newBuilder() + .putRule(serviceEquals("grpc.health.v1.Health"), Sampler.NEVER_SAMPLE) + .putRule(userAuth, RateLimitingSampler.create(100)).build(); + } +} +For more, see https://github.com/openzipkin/brave/tree/master/instrumentation/rpc#sampling-policy +
    +
    +Custom service name +By default, Sleuth assumes that, when you send a span to Zipkin, you want the span’s service name to be equal to the value of the spring.application.name property. +That is not always the case, though. +There are situations in which you want to explicitly provide a different service name for all spans coming from your application. +To achieve that, you can pass the following property to your application to override that value (the example is for a service named myService): +spring.zipkin.service.name: myService +
    +
    +Customization of Reported Spans +Before reporting spans (for example, to Zipkin) you may want to modify that span in some way. +You can do so by using the FinishedSpanHandler interface. +In Sleuth, we generate spans with a fixed name. +Some users want to modify the name depending on values of tags. +You can implement the FinishedSpanHandler interface to alter that name. +The following example shows how to register two beans that implement FinishedSpanHandler: +@Bean +FinishedSpanHandler handlerOne() { + return new FinishedSpanHandler() { + @Override + public boolean handle(TraceContext traceContext, MutableSpan span) { + span.name("foo"); + return true; // keep this span + } + }; +} + +@Bean +FinishedSpanHandler handlerTwo() { + return new FinishedSpanHandler() { + @Override + public boolean handle(TraceContext traceContext, MutableSpan span) { + span.name(span.name() + " bar"); + return true; // keep this span + } + }; +} +The preceding example results in changing the name of the reported span to foo bar, just before it gets reported (for example, to Zipkin). +
    +
    +Host Locator + +This section is about defining host from service discovery. +It is NOT about finding Zipkin through service discovery. + +To define the host that corresponds to a particular span, we need to resolve the host name and port. +The default approach is to take these values from server properties. +If those are not set, we try to retrieve the host name from the network interfaces. +If you have the discovery client enabled and prefer to retrieve the host address from the registered instance in a service registry, you have to set the spring.zipkin.locator.discovery.enabled property (it is applicable for both HTTP-based and Stream-based span reporting), as follows: +spring.zipkin.locator.discovery.enabled: true +
    +
    + +Sending Spans to Zipkin +By default, if you add spring-cloud-starter-zipkin as a dependency to your project, when the span is closed, it is sent to Zipkin over HTTP. +The communication is asynchronous. +You can configure the URL by setting the spring.zipkin.baseUrl property, as follows: +spring.zipkin.baseUrl: https://192.168.99.100:9411/ +If you want to find Zipkin through service discovery, you can pass the Zipkin’s service ID inside the URL, as shown in the following example for zipkinserver service ID: +spring.zipkin.baseUrl: http://zipkinserver/ +To disable this feature just set spring.zipkin.discoveryClientEnabled to `false. +When the Discovery Client feature is enabled, Sleuth uses +LoadBalancerClient to find the URL of the Zipkin Server. It means +that you can set up the load balancing configuration e.g. via Ribbon. +zipkinserver: + ribbon: + ListOfServers: host1,host2 +If you have web, rabbit, or kafka together on the classpath, you might need to pick the means by which you would like to send spans to zipkin. +To do so, set web, rabbit, or kafka to the spring.zipkin.sender.type property. +The following example shows setting the sender type for web: +spring.zipkin.sender.type: web +To customize the RestTemplate that sends spans to Zipkin via HTTP, you can register +the ZipkinRestTemplateCustomizer bean. +@Configuration +class MyConfig { + @Bean ZipkinRestTemplateCustomizer myCustomizer() { + return new ZipkinRestTemplateCustomizer() { + @Override + void customize(RestTemplate restTemplate) { + // customize the RestTemplate + } + }; + } +} +If, however, you would like to control the full process of creating the RestTemplate +object, you will have to create a bean of zipkin2.reporter.Sender type. + @Bean Sender myRestTemplateSender(ZipkinProperties zipkin, + ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer) { + RestTemplate restTemplate = mySuperCustomRestTemplate(); + zipkinRestTemplateCustomizer.customize(restTemplate); + return myCustomSender(zipkin, restTemplate); + } + + +Zipkin Stream Span Consumer + +We recommend using Zipkin’s native support for message-based span sending. +Starting from the Edgware release, the Zipkin Stream server is deprecated. +In the Finchley release, it got removed. + +If for some reason you need to create the deprecated Stream Zipkin server, see the Dalston Documentation. + + +Integrations +
    +OpenTracing +Spring Cloud Sleuth is compatible with OpenTracing. +If you have OpenTracing on the classpath, we automatically register the OpenTracing Tracer bean. +If you wish to disable this, set spring.sleuth.opentracing.enabled to false +
    +
    +Runnable and Callable +If you wrap your logic in Runnable or Callable, you can wrap those classes in their Sleuth representative, as shown in the following example for Runnable: +Runnable runnable = new Runnable() { + @Override + public void run() { + // do some work + } + + @Override + public String toString() { + return "spanNameFromToStringMethod"; + } +}; +// Manual `TraceRunnable` creation with explicit "calculateTax" Span name +Runnable traceRunnable = new TraceRunnable(this.tracing, spanNamer, runnable, + "calculateTax"); +// Wrapping `Runnable` with `Tracing`. That way the current span will be available +// in the thread of `Runnable` +Runnable traceRunnableFromTracer = this.tracing.currentTraceContext() + .wrap(runnable); +The following example shows how to do so for Callable: +Callable<String> callable = new Callable<String>() { + @Override + public String call() throws Exception { + return someLogic(); + } + + @Override + public String toString() { + return "spanNameFromToStringMethod"; + } +}; +// Manual `TraceCallable` creation with explicit "calculateTax" Span name +Callable<String> traceCallable = new TraceCallable<>(this.tracing, spanNamer, + callable, "calculateTax"); +// Wrapping `Callable` with `Tracing`. That way the current span will be available +// in the thread of `Callable` +Callable<String> traceCallableFromTracer = this.tracing.currentTraceContext() + .wrap(callable); +That way, you ensure that a new span is created and closed for each execution. +
    +
    +Hystrix +
    +Custom Concurrency Strategy +We register a custom HystrixConcurrencyStrategy called TraceCallable that wraps all Callable instances in their Sleuth representative. +The strategy either starts or continues a span, depending on whether tracing was already going on before the Hystrix command was called. +To disable the custom Hystrix Concurrency Strategy, set the spring.sleuth.hystrix.strategy.enabled to false. +
    +
    +Manual Command setting +Assume that you have the following HystrixCommand: +HystrixCommand<String> hystrixCommand = new HystrixCommand<String>(setter) { + @Override + protected String run() throws Exception { + return someLogic(); + } +}; +To pass the tracing information, you have to wrap the same logic in the Sleuth version of the HystrixCommand, which is called +TraceCommand, as shown in the following example: +TraceCommand<String> traceCommand = new TraceCommand<String>(tracer, setter) { + @Override + public String doRun() throws Exception { + return someLogic(); + } +}; +
    +
    +
    +RxJava +We registering a custom RxJavaSchedulersHook that wraps all Action0 instances in their Sleuth representative, which is called TraceAction. +The hook either starts or continues a span, depending on whether tracing was already going on before the Action was scheduled. +To disable the custom RxJavaSchedulersHook, set the spring.sleuth.rxjava.schedulers.hook.enabled to false. +You can define a list of regular expressions for thread names for which you do not want spans to be created. +To do so, provide a comma-separated list of regular expressions in the spring.sleuth.rxjava.schedulers.ignoredthreads property. + +The suggest approach to reactive programming and Sleuth is to use +the Reactor support. + +
    +
    +HTTP integration +Features from this section can be disabled by setting the spring.sleuth.web.enabled property with value equal to false. +
    +HTTP Filter +Through the TracingFilter, all sampled incoming requests result in creation of a Span. +That Span’s name is http: + the path to which the request was sent. +For example, if the request was sent to /this/that then the name will be http:/this/that. +You can configure which URIs you would like to skip by setting the spring.sleuth.web.skipPattern property. +If you have ManagementServerProperties on classpath, its value of contextPath gets appended to the provided skip pattern. +If you want to reuse the Sleuth’s default skip patterns and just append your own, pass those patterns by using the spring.sleuth.web.additionalSkipPattern. +By default, all the spring boot actuator endpoints are automatically added to the skip pattern. +If you want to disable this behaviour set spring.sleuth.web.ignore-auto-configured-skip-patterns +to true. +To change the order of tracing filter registration, please set the +spring.sleuth.web.filter-order property. +To disable the filter that logs uncaught exceptions you can disable the +spring.sleuth.web.exception-throwing-filter-enabled property. +
    +
    +HandlerInterceptor +Since we want the span names to be precise, we use a TraceHandlerInterceptor that either wraps an existing HandlerInterceptor or is added directly to the list of existing HandlerInterceptors. +The TraceHandlerInterceptor adds a special request attribute to the given HttpServletRequest. +If the the TracingFilter does not see this attribute, it creates a fallback span, which is an additional span created on the server side so that the trace is presented properly in the UI. +If that happens, there is probably missing instrumentation. +In that case, please file an issue in Spring Cloud Sleuth. +
    +
    +Async Servlet support +If your controller returns a Callable or a WebAsyncTask, Spring Cloud Sleuth continues the existing span instead of creating a new one. +
    +
    +WebFlux support +Through TraceWebFilter, all sampled incoming requests result in creation of a Span. +That Span’s name is http: + the path to which the request was sent. +For example, if the request was sent to /this/that, the name is http:/this/that. +You can configure which URIs you would like to skip by using the spring.sleuth.web.skipPattern property. +If you have ManagementServerProperties on the classpath, its value of contextPath gets appended to the provided skip pattern. +If you want to reuse Sleuth’s default skip patterns and append your own, pass those patterns by using the spring.sleuth.web.additionalSkipPattern. +To change the order of tracing filter registration, please set the +spring.sleuth.web.filter-order property. +
    +
    +Dubbo RPC support +Via the integration with Brave, Spring Cloud Sleuth supports Dubbo. +It’s enough to add the brave-instrumentation-dubbo dependency: +<dependency> + <groupId>io.zipkin.brave</groupId> + <artifactId>brave-instrumentation-dubbo</artifactId> +</dependency> +You need to also set a dubbo.properties file with the following contents: +dubbo.provider.filter=tracing +dubbo.consumer.filter=tracing +You can read more about Brave - Dubbo integration here. +An example of Spring Cloud Sleuth and Dubbo can be found here. +
    +
    +
    +HTTP Client Integration +
    +Synchronous Rest Template +We inject a RestTemplate interceptor to ensure that all the tracing information is passed to the requests. +Each time a call is made, a new Span is created. +It gets closed upon receiving the response. +To block the synchronous RestTemplate features, set spring.sleuth.web.client.enabled to false. + +You have to register RestTemplate as a bean so that the interceptors get injected. +If you create a RestTemplate instance with a new keyword, the instrumentation does NOT work. + +
    +
    +Asynchronous Rest Template + +Starting with Sleuth 2.0.0, we no longer register a bean of AsyncRestTemplate type. +It is up to you to create such a bean. +Then we instrument it. + +To block the AsyncRestTemplate features, set spring.sleuth.web.async.client.enabled to false. +To disable creation of the default TraceAsyncClientHttpRequestFactoryWrapper, set spring.sleuth.web.async.client.factory.enabled +to false. +If you do not want to create AsyncRestClient at all, set spring.sleuth.web.async.client.template.enabled to false. +
    +Multiple Asynchronous Rest Templates +Sometimes you need to use multiple implementations of the Asynchronous Rest Template. +In the following snippet, you can see an example of how to set up such a custom AsyncRestTemplate: +@Configuration +@EnableAutoConfiguration +static class Config { + + @Bean(name = "customAsyncRestTemplate") + public AsyncRestTemplate traceAsyncRestTemplate() { + return new AsyncRestTemplate(asyncClientFactory(), + clientHttpRequestFactory()); + } + + private ClientHttpRequestFactory clientHttpRequestFactory() { + ClientHttpRequestFactory clientHttpRequestFactory = new CustomClientHttpRequestFactory(); + // CUSTOMIZE HERE + return clientHttpRequestFactory; + } + + private AsyncClientHttpRequestFactory asyncClientFactory() { + AsyncClientHttpRequestFactory factory = new CustomAsyncClientHttpRequestFactory(); + // CUSTOMIZE HERE + return factory; + } + +} +
    +
    +
    +<literal>WebClient</literal> +We inject a ExchangeFilterFunction implementation that creates a span and, through on-success and on-error callbacks, takes care of closing client-side spans. +To block this feature, set spring.sleuth.web.client.enabled to false. + +You have to register WebClient as a bean so that the tracing instrumentation gets applied. +If you create a WebClient instance with a new keyword, the instrumentation does NOT work. + +
    +
    +Traverson +If you use the Traverson library, you can inject a RestTemplate as a bean into your Traverson object. +Since RestTemplate is already intercepted, you get full support for tracing in your client. The following pseudo code +shows how to do that: +@Autowired RestTemplate restTemplate; + +Traverson traverson = new Traverson(URI.create("http://some/address"), + MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8).setRestOperations(restTemplate); +// use Traverson +
    +
    +Apache <literal>HttpClientBuilder</literal> and <literal>HttpAsyncClientBuilder</literal> +We instrument the HttpClientBuilder and HttpAsyncClientBuilder so that +tracing context gets injected to the sent requests. +To block these features, set spring.sleuth.web.client.enabled to false. +
    +
    +Netty <literal>HttpClient</literal> +We instrument the Netty’s HttpClient. +To block this feature, set spring.sleuth.web.client.enabled to false. + +You have to register HttpClient as a bean so that the instrumentation happens. +If you create a HttpClient instance with a new keyword, the instrumentation does NOT work. + +
    +
    +<literal>UserInfoRestTemplateCustomizer</literal> +We instrument the Spring Security’s UserInfoRestTemplateCustomizer. +To block this feature, set spring.sleuth.web.client.enabled to false. +
    +
    +
    +Feign +By default, Spring Cloud Sleuth provides integration with Feign through TraceFeignClientAutoConfiguration. +You can disable it entirely by setting spring.sleuth.feign.enabled to false. +If you do so, no Feign-related instrumentation take place. +Part of Feign instrumentation is done through a FeignBeanPostProcessor. +You can disable it by setting spring.sleuth.feign.processor.enabled to false. +If you set it to false, Spring Cloud Sleuth does not instrument any of your custom Feign components. +However, all the default instrumentation is still there. +
    +
    +gRPC +Spring Cloud Sleuth provides instrumentation for gRPC through TraceGrpcAutoConfiguration. You can disable it entirely by setting spring.sleuth.grpc.enabled to false. +
    +Variant 1 +
    +Dependencies + +The gRPC integration relies on two external libraries to instrument clients and servers and both of those libraries must be on the class path to enable the instrumentation. + +Maven: + <dependency> + <groupId>io.github.lognet</groupId> + <artifactId>grpc-spring-boot-starter</artifactId> + </dependency> + <dependency> + <groupId>io.zipkin.brave</groupId> + <artifactId>brave-instrumentation-grpc</artifactId> + </dependency> +Gradle: + compile("io.github.lognet:grpc-spring-boot-starter") + compile("io.zipkin.brave:brave-instrumentation-grpc") +
    +
    +Server Instrumentation +Spring Cloud Sleuth leverages grpc-spring-boot-starter to register Brave’s gRPC server interceptor with all services annotated with @GRpcService. +
    +
    +Client Instrumentation +gRPC clients leverage a ManagedChannelBuilder to construct a ManagedChannel used to communicate to the gRPC server. The native ManagedChannelBuilder provides static methods as entry points for construction of ManagedChannel instances, however, this mechanism is outside the influence of the Spring application context. + +Spring Cloud Sleuth provides a SpringAwareManagedChannelBuilder that can be customized through the Spring application context and injected by gRPC clients. This builder must be used when creating ManagedChannel instances. + +Sleuth creates a TracingManagedChannelBuilderCustomizer which inject Brave’s client interceptor into the SpringAwareManagedChannelBuilder. +
    +
    +
    +Variant 2 +Grpc Spring Boot Starter automatically detects the presence of Spring Cloud Sleuth and brave’s instrumentation for gRPC and registers the necessary client and/or server tooling. +
    +
    +
    +Asynchronous Communication +
    +<literal>@Async</literal> Annotated methods +In Spring Cloud Sleuth, we instrument async-related components so that the tracing information is passed between threads. +You can disable this behavior by setting the value of spring.sleuth.async.enabled to false. +If you annotate your method with @Async, we automatically create a new Span with the following characteristics: + + +If the method is annotated with @SpanName, the value of the annotation is the Span’s name. + + +If the method is not annotated with @SpanName, the Span name is the annotated method name. + + +The span is tagged with the method’s class name and method name. + + +
    +
    +<literal>@Scheduled</literal> Annotated Methods +In Spring Cloud Sleuth, we instrument scheduled method execution so that the tracing information is passed between threads. +You can disable this behavior by setting the value of spring.sleuth.scheduled.enabled to false. +If you annotate your method with @Scheduled, we automatically create a new span with the following characteristics: + + +The span name is the annotated method name. + + +The span is tagged with the method’s class name and method name. + + +If you want to skip span creation for some @Scheduled annotated classes, you can set the spring.sleuth.scheduled.skipPattern with a regular expression that matches the fully qualified name of the @Scheduled annotated class. +If you use spring-cloud-sleuth-stream and spring-cloud-netflix-hystrix-stream together, a span is created for each Hystrix metrics and sent to Zipkin. +This behavior may be annoying. That’s why, by default, spring.sleuth.scheduled.skipPattern=org.springframework.cloud.netflix.hystrix.stream.HystrixStreamTask. +
    +
    +Executor, ExecutorService, and ScheduledExecutorService +We provide LazyTraceExecutor, TraceableExecutorService, and TraceableScheduledExecutorService. Those implementations create spans each time a new task is submitted, invoked, or scheduled. +The following example shows how to pass tracing information with TraceableExecutorService when working with CompletableFuture: +CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> { + // perform some logic + return 1_000_000L; +}, new TraceableExecutorService(beanFactory, executorService, + // 'calculateTax' explicitly names the span - this param is optional + "calculateTax")); + +Sleuth does not work with parallelStream() out of the box. +If you want to have the tracing information propagated through the stream, you have to use the approach with supplyAsync(…​), as shown earlier. + +If there are beans that implement the Executor interface that you would like +to exclude from span creation, you can use the spring.sleuth.async.ignored-beans +property where you can provide a list of bean names. +
    +Customization of Executors +Sometimes, you need to set up a custom instance of the AsyncExecutor. +The following example shows how to set up such a custom Executor: +@Configuration +@EnableAutoConfiguration +@EnableAsync +// add the infrastructure role to ensure that the bean gets auto-proxied +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +static class CustomExecutorConfig extends AsyncConfigurerSupport { + + @Autowired + BeanFactory beanFactory; + + @Override + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + // CUSTOMIZE HERE + executor.setCorePoolSize(7); + executor.setMaxPoolSize(42); + executor.setQueueCapacity(11); + executor.setThreadNamePrefix("MyExecutor-"); + // DON'T FORGET TO INITIALIZE + executor.initialize(); + return new LazyTraceExecutor(this.beanFactory, executor); + } + +} + +To ensure that your configuration gets post processed, remember +to add the @Role(BeanDefinition.ROLE_INFRASTRUCTURE) on your +@Configuration class + +
    +
    +
    +
    +Messaging +Features from this section can be disabled by setting the spring.sleuth.messaging.enabled property with value equal to false. +
    +Spring Integration and Spring Cloud Stream +Spring Cloud Sleuth integrates with Spring Integration. +It creates spans for publish and subscribe events. +To disable Spring Integration instrumentation, set spring.sleuth.integration.enabled to false. +You can provide the spring.sleuth.integration.patterns pattern to explicitly provide the names of channels that you want to include for tracing. +By default, all channels but hystrixStreamOutput channel are included. + +When using the Executor to build a Spring Integration IntegrationFlow, you must use the untraced version of the Executor. +Decorating the Spring Integration Executor Channel with TraceableExecutorService causes the spans to be improperly closed. + +If you want to customize the way tracing context is read from and written to message headers, +it’s enough for you to register beans of types: + + +Propagation.Setter<MessageHeaderAccessor, String> - for writing headers to the message + + +Propagation.Getter<MessageHeaderAccessor, String> - for reading headers from the message + + +
    +
    +Spring RabbitMq +We instrument the RabbitTemplate so that tracing headers get injected +into the message. +To block this feature, set spring.sleuth.messaging.rabbit.enabled to false. +
    +
    +Spring Kafka +We instrument the Spring Kafka’s ProducerFactory and ConsumerFactory +so that tracing headers get injected into the created Spring Kafka’s +Producer and Consumer. +To block this feature, set spring.sleuth.messaging.kafka.enabled to false. +
    +
    +Spring JMS +We instrument the JmsTemplate so that tracing headers get injected +into the message. We also support @JmsListener annotated methods on the consumer side. +To block this feature, set spring.sleuth.messaging.jms.enabled to false. + +We don’t support baggage propagation for JMS + +
    +
    +
    +Zuul +We instrument the Zuul Ribbon integration by enriching the Ribbon requests with tracing information. +To disable Zuul support, set the spring.sleuth.zuul.enabled property to false. +
    +
    +Project Reactor +For projects depending on Project Reactor such as Spring Cloud Gateway, we suggest turning the spring.sleuth.reactor.decorate-on-each option to false. That way an increased performance gain should be observed in comparison to the standard instrumentation mechanism. What this option does is it will wrap decorate onLast operator instead of onEach which will result in creation of far fewer objects. The downside of this is that when Project Reactor will change threads, the trace propagation will continue without issues, however anything relying on the ThreadLocal such as e.g. MDC entries can be buggy. +
    +
    + +Running examples +You can see the running examples deployed in the Pivotal Web Services. +Check them out at the following links: + + +Zipkin for apps presented in the samples to the top. First make +a request to Service 1 and then check out the trace in Zipkin. + + +Zipkin for Brewery on PWS, its Github Code. +Ensure that you’ve picked the lookback period of 7 days. If there are no traces, go to Presenting application +and order some beers. Then check Zipkin for traces. + + + +
    + +Spring Cloud Consul + +Greenwich.SR5 +This project provides Consul integrations for Spring Boot apps through autoconfiguration +and binding to the Spring Environment and other Spring programming model idioms. With a few +simple annotations you can quickly enable and configure the common patterns inside your +application and build large distributed systems with Consul based components. The +patterns provided include Service Discovery, Control Bus and Configuration. +Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon), Circuit Breaker +(Hystrix) are provided by integration with Spring Cloud Netflix. + + +Install Consul +Please see the installation documentation for instructions on how to install Consul. + + +Consul Agent +A Consul Agent client must be available to all Spring Cloud Consul applications. By default, the Agent client is expected to be at localhost:8500. See the Agent documentation for specifics on how to start an Agent client and how to connect to a cluster of Consul Agent Servers. For development, after you have installed consul, you may start a Consul Agent using the following command: +./src/main/bash/local_run_consul.sh +This will start an agent in server mode on port 8500, with the ui available at http://localhost:8500 + + +Service Discovery with Consul +Service Discovery is one of the key tenets of a microservice based architecture. Trying to hand configure each client or some form of convention can be very difficult to do and can be very brittle. Consul provides Service Discovery services via an HTTP API and DNS. Spring Cloud Consul leverages the HTTP API for service registration and discovery. This does not prevent non-Spring Cloud applications from leveraging the DNS interface. Consul Agents servers are run in a cluster that communicates via a gossip protocol and uses the Raft consensus protocol. +
    +How to activate +To activate Consul Service Discovery use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-discovery. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +
    +
    +Registering with Consul +When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An HTTP Check is created by default that Consul hits the /health endpoint every 10 seconds. If the health check fails, the service instance is marked as critical. +Example Consul client: +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} +(i.e. utterly normal Spring Boot app). If the Consul client is located somewhere other than localhost:8500, the configuration is required to locate the client. Example: + +application.yml + +spring: + cloud: + consul: + host: localhost + port: 8500 + + + +If you use Spring Cloud Consul Config, the above values will need to be placed in bootstrap.yml instead of application.yml. + +The default service name, instance id and port, taken from the Environment, are ${spring.application.name}, the Spring Context ID and ${server.port} respectively. +To disable the Consul Discovery Client you can set spring.cloud.consul.discovery.enabled to false. Consul Discovery Client will also be disabled when spring.cloud.discovery.enabled is set to false. +To disable the service registration you can set spring.cloud.consul.discovery.register to false. +
    +Registering Management as a Separate Service +When management server port is set to something different than the application port, by setting management.server.port property, management service will be registered as a separate service than the application service. For example: + +application.yml + +spring: + application: + name: myApp +management: + server: + port: 4452 + + +Above configuration will register following 2 services: + + +Application Service: + + +ID: myApp +Name: myApp + + +Management Service: + + +ID: myApp-management +Name: myApp-management +Management service will inherit its instanceId and serviceName from the application service. For example: + +application.yml + +spring: + application: + name: myApp +management: + server: + port: 4452 +spring: + cloud: + consul: + discovery: + instance-id: custom-service-id + serviceName: myprefix-${spring.application.name} + + +Above configuration will register following 2 services: + + +Application Service: + + +ID: custom-service-id +Name: myprefix-myApp + + +Management Service: + + +ID: custom-service-id-management +Name: myprefix-myApp-management +Further customization is possible via following properties: +/** Port to register the management service under (defaults to management port) */ +spring.cloud.consul.discovery.management-port + +/** Suffix to use when registering management service (defaults to "management" */ +spring.cloud.consul.discovery.management-suffix + +/** Tags to use when registering management service (defaults to "management" */ +spring.cloud.consul.discovery.management-tags +
    +
    +
    +HTTP Health Check +The health check for a Consul instance defaults to "/health", which is the default locations of a useful endpoint in a Spring Boot Actuator application. You need to change these, even for an Actuator application if you use a non-default context path or servlet path (e.g. server.servletPath=/foo) or management endpoint path (e.g. management.server.servlet.context-path=/admin). The interval that Consul uses to check the health endpoint may also be configured. "10s" and "1m" represent 10 seconds and 1 minute respectively. Example: + +application.yml + +spring: + cloud: + consul: + discovery: + healthCheckPath: ${management.server.servlet.context-path}/health + healthCheckInterval: 15s + + +You can disable the health check by setting management.health.consul.enabled=false. +
    +Metadata and Consul tags +Consul does not yet support metadata on services. Spring Cloud’s ServiceInstance has a Map<String, String> metadata field. Spring Cloud Consul uses Consul tags to approximate metadata until Consul officially supports metadata. Tags with the form key=value will be split and used as a Map key and value respectively. Tags without the equal = sign, will be used as both the key and value. + +application.yml + +spring: + cloud: + consul: + discovery: + tags: foo=bar, baz + + +The above configuration will result in a map with foo→bar and baz→baz. +
    +
    +Making the Consul Instance ID Unique +By default a consul instance is registered with an ID that is equal to its Spring Application Context ID. By default, the Spring Application Context ID is ${spring.application.name}:comma,separated,profiles:${server.port}. For most cases, this will allow multiple instances of one service to run on one machine. If further uniqueness is required, Using Spring Cloud you can override this by providing a unique identifier in spring.cloud.consul.discovery.instanceId. For example: + +application.yml + +spring: + cloud: + consul: + discovery: + instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}} + + +With this metadata, and multiple service instances deployed on localhost, the random value will kick in there to make the instance unique. In Cloudfoundry the vcap.application.instance_id will be populated automatically in a Spring Boot application, so the random value will not be needed. +
    +
    +Applying Headers to Health Check Requests +Headers can be applied to health check requests. For example, if you’re trying to register a Spring Cloud Config server that uses Vault Backend: + +application.yml + +spring: + cloud: + consul: + discovery: + health-check-headers: + X-Config-Token: 6442e58b-d1ea-182e-cfa5-cf9cddef0722 + + +According to the HTTP standard, each header can have more than one values, in which case, an array can be supplied: + +application.yml + +spring: + cloud: + consul: + discovery: + health-check-headers: + X-Config-Token: + - "6442e58b-d1ea-182e-cfa5-cf9cddef0722" + - "Some other value" + + +
    +
    +
    +Looking up services +
    +Using Ribbon +Spring Cloud has support for Feign (a REST client builder) and also Spring RestTemplate +for looking up services using the logical service names/ids instead of physical URLs. Both Feign and the discovery-aware RestTemplate utilize Ribbon for client-side load balancing. +If you want to access service STORES using the RestTemplate simply declare: +@LoadBalanced +@Bean +public RestTemplate loadbalancedRestTemplate() { + new RestTemplate(); +} +and use it like this (notice how we use the STORES service name/id from Consul instead of a fully qualified domainname): +@Autowired +RestTemplate restTemplate; + +public String getFirstProduct() { + return this.restTemplate.getForObject("https://STORES/products/1", String.class); +} +If you have Consul clusters in multiple datacenters and you want to access a service in another datacenter a service name/id alone is not enough. In that case +you use property spring.cloud.consul.discovery.datacenters.STORES=dc-west where STORES is the service name/id and dc-west is the datacenter +where the STORES service lives. +
    +
    +Using the DiscoveryClient +You can also use the org.springframework.cloud.client.discovery.DiscoveryClient which provides a simple API for discovery clients that is not specific to Netflix, e.g. +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List<ServiceInstance> list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri(); + } + return null; +} +
    +
    +
    +Consul Catalog Watch +The Consul Catalog Watch takes advantage of the ability of consul to watch services. The Catalog Watch makes a blocking Consul HTTP API call to determine if any services have changed. If there is new service data a Heartbeat Event is published. +To change the frequency of when the Config Watch is called change spring.cloud.consul.config.discovery.catalog-services-watch-delay. The default value is 1000, which is in milliseconds. The delay is the amount of time after the end of the previous invocation and the start of the next. +To disable the Catalog Watch set spring.cloud.consul.discovery.catalogServicesWatch.enabled=false. +The watch uses a Spring TaskScheduler to schedule the call to consul. By default it is a ThreadPoolTaskScheduler with a poolSize of 1. To change the TaskScheduler, create a bean of type TaskScheduler named with the ConsulDiscoveryClientConfiguration.CATALOG_WATCH_TASK_SCHEDULER_NAME constant. +
    +
    + +Distributed Configuration with Consul +Consul provides a Key/Value Store for storing configuration and other metadata. Spring Cloud Consul Config is an alternative to the Config Server and Client. Configuration is loaded into the Spring Environment during the special "bootstrap" phase. Configuration is stored in the /config folder by default. Multiple PropertySource instances are created based on the application’s name and the active profiles that mimicks the Spring Cloud Config order of resolving properties. For example, an application with the name "testApp" and with the "dev" profile will have the following property sources created: +config/testApp,dev/ +config/testApp/ +config/application,dev/ +config/application/ +The most specific property source is at the top, with the least specific at the bottom. Properties in the config/application folder are applicable to all applications using consul for configuration. Properties in the config/testApp folder are only available to the instances of the service named "testApp". +Configuration is currently read on startup of the application. Sending a HTTP POST to /refresh will cause the configuration to be reloaded. will also automatically detect changes and reload the application context. +
    +How to activate +To get started with Consul Configuration use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-config. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +This will enable auto-configuration that will setup Spring Cloud Consul Config. +
    +
    +Customizing +Consul Config may be customized using the following properties: + +bootstrap.yml + +spring: + cloud: + consul: + config: + enabled: true + prefix: configuration + defaultContext: apps + profileSeparator: '::' + + + + +enabled setting this value to "false" disables Consul Config + + +prefix sets the base folder for configuration values + + +defaultContext sets the folder name used by all applications + + +profileSeparator sets the value of the separator used to separate the profile name in property sources with profiles + + +
    +
    +Config Watch +The Consul Config Watch takes advantage of the ability of consul to watch a key prefix. The Config Watch makes a blocking Consul HTTP API call to determine if any relevant configuration data has changed for the current application. If there is new configuration data a Refresh Event is published. This is equivalent to calling the /refresh actuator endpoint. +To change the frequency of when the Config Watch is called change spring.cloud.consul.config.watch.delay. The default value is 1000, which is in milliseconds. The delay is the amount of time after the end of the previous invocation and the start of the next. +To disable the Config Watch set spring.cloud.consul.config.watch.enabled=false. +The watch uses a Spring TaskScheduler to schedule the call to consul. By default it is a ThreadPoolTaskScheduler with a poolSize of 1. To change the TaskScheduler, create a bean of type TaskScheduler named with the ConsulConfigAutoConfiguration.CONFIG_WATCH_TASK_SCHEDULER_NAME constant. +
    +
    +YAML or Properties with Config +It may be more convenient to store a blob of properties in YAML or Properties format as opposed to individual key/value pairs. Set the spring.cloud.consul.config.format property to YAML or PROPERTIES. For example to use YAML: + +bootstrap.yml + +spring: + cloud: + consul: + config: + format: YAML + + +YAML must be set in the appropriate data key in consul. Using the defaults above the keys would look like: +config/testApp,dev/data +config/testApp/data +config/application,dev/data +config/application/data +You could store a YAML document in any of the keys listed above. +You can change the data key using spring.cloud.consul.config.data-key. +
    +
    +git2consul with Config +git2consul is a Consul community project that loads files from a git repository to individual keys into Consul. By default the names of the keys are names of the files. YAML and Properties files are supported with file extensions of .yml and .properties respectively. Set the spring.cloud.consul.config.format property to FILES. For example: + +bootstrap.yml + +spring: + cloud: + consul: + config: + format: FILES + + +Given the following keys in /config, the development profile and an application name of foo: +.gitignore +application.yml +bar.properties +foo-development.properties +foo-production.yml +foo.properties +master.ref +the following property sources would be created: +config/foo-development.properties +config/foo.properties +config/application.yml +The value of each key needs to be a properly formatted YAML or Properties file. +
    +
    +Fail Fast +It may be convenient in certain circumstances (like local development or certain test scenarios) to not fail if consul isn’t available for configuration. Setting spring.cloud.consul.config.failFast=false in bootstrap.yml will cause the configuration module to log a warning rather than throw an exception. This will allow the application to continue startup normally. +
    +
    + +Consul Retry +If you expect that the consul agent may occasionally be unavailable when +your app starts, you can ask it to keep trying after a failure. You need to add +spring-retry and spring-boot-starter-aop to your classpath. The default +behaviour is to retry 6 times with an initial backoff interval of 1000ms and an +exponential multiplier of 1.1 for subsequent backoffs. You can configure these +properties (and others) using spring.cloud.consul.retry.* configuration properties. +This works with both Spring Cloud Consul Config and Discovery registration. + +To take full control of the retry add a @Bean of type +RetryOperationsInterceptor with id "consulRetryInterceptor". Spring +Retry has a RetryInterceptorBuilder that makes it easy to create one. + + + +Spring Cloud Bus with Consul +
    +How to activate +To get started with the Consul Bus use the starter with group org.springframework.cloud and artifact id spring-cloud-starter-consul-bus. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train. +See the Spring Cloud Bus documentation for the available actuator endpoints and howto send custom messages. +
    +
    + +Circuit Breaker with Hystrix +Applications can use the Hystrix Circuit Breaker provided by the Spring Cloud Netflix project by including this starter in the projects pom.xml: spring-cloud-starter-hystrix. Hystrix doesn’t depend on the Netflix Discovery Client. The @EnableHystrix annotation should be placed on a configuration class (usually the main class). Then methods can be annotated with @HystrixCommand to be protected by a circuit breaker. See the documentation for more details. + + +Hystrix metrics aggregation with Turbine and Consul +Turbine (provided by the Spring Cloud Netflix project), aggregates multiple instances Hystrix metrics streams, so the dashboard can display an aggregate view. Turbine uses the DiscoveryClient interface to lookup relevant instances. To use Turbine with Spring Cloud Consul, configure the Turbine application in a manner similar to the following examples: + +pom.xml + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-netflix-turbine</artifactId> +</dependency> +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-consul-discovery</artifactId> +</dependency> + + +Notice that the Turbine dependency is not a starter. The turbine starter includes support for Netflix Eureka. + +application.yml + +spring.application.name: turbine +applications: consulhystrixclient +turbine: + aggregator: + clusterConfig: ${applications} + appConfig: ${applications} + + +The clusterConfig and appConfig sections must match, so it’s useful to put the comma-separated list of service ID’s into a separate configuration property. + +Turbine.java + +@EnableTurbine +@SpringBootApplication +public class Turbine { + public static void main(String[] args) { + SpringApplication.run(DemoturbinecommonsApplication.class, args); + } +} + + + +
    + +Spring Cloud Zookeeper + +This project provides Zookeeper integrations for Spring Boot applications through +autoconfiguration and binding to the Spring Environment and other Spring programming model +idioms. With a few annotations, you can quickly enable and configure the common patterns +inside your application and build large distributed systems with Zookeeper based +components. The provided patterns include Service Discovery and Configuration. Integration +with Spring Cloud Netflix provides Intelligent Routing (Zuul), Client Side Load Balancing +(Ribbon), and Circuit Breaker (Hystrix). + + +Install Zookeeper +See the installation +documentation for instructions on how to install Zookeeper. +Spring Cloud Zookeeper uses Apache Curator behind the scenes. +While Zookeeper 3.5.x is still considered "beta" by the Zookeeper development team, +the reality is that it is used in production by many users. +However, Zookeeper 3.4.x is also used in production. +Prior to Apache Curator 4.0, both versions of Zookeeper were supported via two versions of Apache Curator. +Starting with Curator 4.0 both versions of Zookeeper are supported via the same Curator libraries. +In case you are integrating with version 3.4 you need to change the Zookeeper dependency +that comes shipped with curator, and thus spring-cloud-zookeeper. +To do so simply exclude that dependency and add the 3.4.x version like shown below. + +maven + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-zookeeper-all</artifactId> + <exclusions> + <exclusion> + <groupId>org.apache.zookeeper</groupId> + <artifactId>zookeeper</artifactId> + </exclusion> + </exclusions> +</dependency> +<dependency> + <groupId>org.apache.zookeeper</groupId> + <artifactId>zookeeper</artifactId> + <version>3.4.12</version> + <exclusions> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + </exclusion> + </exclusions> +</dependency> + + + +gradle + +compile('org.springframework.cloud:spring-cloud-starter-zookeeper-all') { + exclude group: 'org.apache.zookeeper', module: 'zookeeper' +} +compile('org.apache.zookeeper:zookeeper:3.4.12') { + exclude group: 'org.slf4j', module: 'slf4j-log4j12' +} + + + + +Service Discovery with Zookeeper +Service Discovery is one of the key tenets of a microservice based architecture. Trying to +hand-configure each client or some form of convention can be difficult to do and can be +brittle. Curator(A Java library for Zookeeper) provides Service +Discovery through a Service Discovery +Extension. Spring Cloud Zookeeper uses this extension for service registration and +discovery. +
    +Activating +Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-discovery enables +autoconfiguration that sets up Spring Cloud Zookeeper Discovery. + +For web functionality, you still need to include +org.springframework.boot:spring-boot-starter-web. + + +When working with version 3.4 of Zookeeper you need to change +the way you include the dependency as described here. + +
    +
    +Registering with Zookeeper +When a client registers with Zookeeper, it provides metadata (such as host and port, ID, +and name) about itself. +The following example shows a Zookeeper client: +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} + +The preceding example is a normal Spring Boot application. + +If Zookeeper is located somewhere other than localhost:2181, the configuration must +provide the location of the server, as shown in the following example: + +application.yml + +spring: + cloud: + zookeeper: + connect-string: localhost:2181 + + + +If you use Spring Cloud Zookeeper Config, the +values shown in the preceding example need to be in bootstrap.yml instead of +application.yml. + +The default service name, instance ID, and port (taken from the Environment) are +${spring.application.name}, the Spring Context ID, and ${server.port}, respectively. +Having spring-cloud-starter-zookeeper-discovery on the classpath makes the app into both +a Zookeeper service (that is, it registers itself) and a client (that is, it can +query Zookeeper to locate other services). +If you would like to disable the Zookeeper Discovery Client, you can set +spring.cloud.zookeeper.discovery.enabled to false. +
    +
    +Using the DiscoveryClient +Spring Cloud has support for +Feign +(a REST client builder) and +Spring +RestTemplate, using logical service names instead of physical URLs. +You can also use the org.springframework.cloud.client.discovery.DiscoveryClient, which +provides a simple API for discovery clients that is not specific to Netflix, as shown in +the following example: +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List<ServiceInstance> list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri().toString(); + } + return null; +} +
    +
    + +Using Spring Cloud Zookeeper with Spring Cloud Netflix Components +Spring Cloud Netflix supplies useful tools that work regardless of which DiscoveryClient +implementation you use. Feign, Turbine, Ribbon, and Zuul all work with Spring Cloud +Zookeeper. +
    +Ribbon with Zookeeper +Spring Cloud Zookeeper provides an implementation of Ribbon’s ServerList. When you use +the spring-cloud-starter-zookeeper-discovery, Ribbon is autoconfigured to use the +ZookeeperServerList by default. +
    +
    + +Spring Cloud Zookeeper and Service Registry +Spring Cloud Zookeeper implements the ServiceRegistry interface, letting developers +register arbitrary services in a programmatic way. +The ServiceInstanceRegistration class offers a builder() method to create a +Registration object that can be used by the ServiceRegistry, as shown in the following +example: +@Autowired +private ZookeeperServiceRegistry serviceRegistry; + +public void registerThings() { + ZookeeperRegistration registration = ServiceInstanceRegistration.builder() + .defaultUriSpec() + .address("anyUrl") + .port(10) + .name("/a/b/c/d/anotherservice") + .build(); + this.serviceRegistry.register(registration); +} +
    +Instance Status +Netflix Eureka supports having instances that are OUT_OF_SERVICE registered with the +server. These instances are not returned as active service instances. This is useful for +behaviors such as blue/green deployments. (Note that the Curator Service Discovery recipe +does not support this behavior.) Taking advantage of the flexible payload has let Spring +Cloud Zookeeper implement OUT_OF_SERVICE by updating some specific metadata and then +filtering on that metadata in the Ribbon ZookeeperServerList. The ZookeeperServerList +filters out all non-null instance statuses that do not equal UP. If the instance status +field is empty, it is considered to be UP for backwards compatibility. To change the +status of an instance, make a POST with OUT_OF_SERVICE to the ServiceRegistry +instance status actuator endpoint, as shown in the following example: +$ http POST http://localhost:8081/service-registry status=OUT_OF_SERVICE + +The preceding example uses the http command from https://httpie.org. + +
    +
    + +Zookeeper Dependencies +The following topics cover how to work with Spring Cloud Zookeeper dependencies: + + + + + + + + + + + + + + +
    +Using the Zookeeper Dependencies +Spring Cloud Zookeeper gives you a possibility to provide dependencies of your application +as properties. As dependencies, you can understand other applications that are registered +in Zookeeper and which you would like to call through +Feign +(a REST client builder) and Spring RestTemplate. +You can also use the Zookeeper Dependency Watchers functionality to control and monitor +the state of your dependencies. +
    +
    +Activating Zookeeper Dependencies +Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-discovery enables +autoconfiguration that sets up Spring Cloud Zookeeper Dependencies. Even if you provide +the dependencies in your properties, you can turn off the dependencies. To do so, set the +spring.cloud.zookeeper.dependency.enabled property to false (it defaults to true). +
    +
    +Setting up Zookeeper Dependencies +Consider the following example of dependency representation: + +application.yml + +spring.application.name: yourServiceName +spring.cloud.zookeeper: + dependencies: + newsletter: + path: /path/where/newsletter/has/registered/in/zookeeper + loadBalancerType: ROUND_ROBIN + contentTypeTemplate: application/vnd.newsletter.$version+json + version: v1 + headers: + header1: + - value1 + header2: + - value2 + required: false + stubs: org.springframework:foo:stubs + mailing: + path: /path/where/mailing/has/registered/in/zookeeper + loadBalancerType: ROUND_ROBIN + contentTypeTemplate: application/vnd.mailing.$version+json + version: v1 + required: true + + +The next few sections go through each part of the dependency one by one. The root property +name is spring.cloud.zookeeper.dependencies. +
    +Aliases +Below the root property you have to represent each dependency as an alias. This is due to +the constraints of Ribbon, which requires that the application ID be placed in the URL. +Consequently, you cannot pass any complex path, suchas /myApp/myRoute/name). The alias +is the name you use instead of the serviceId for DiscoveryClient, Feign, or +RestTemplate. +In the previous examples, the aliases are newsletter and mailing. The following +example shows Feign usage with a newsletter alias: +@FeignClient("newsletter") +public interface NewsletterService { + @RequestMapping(method = RequestMethod.GET, value = "/newsletter") + String getNewsletters(); +} +
    +
    +Path +The path is represented by the path YAML property and is the path under which the +dependency is registered under Zookeeper. As described in the +previous section, Ribbon +operates on URLs. As a result, this path is not compliant with its requirement. +That is why Spring Cloud Zookeeper maps the alias to the proper path. +
    +
    +Load Balancer Type +The load balancer type is represented by loadBalancerType YAML property. +If you know what kind of load-balancing strategy has to be applied when calling this +particular dependency, you can provide it in the YAML file, and it is automatically +applied. You can choose one of the following load balancing strategies: + + +STICKY: Once chosen, the instance is always called. + + +RANDOM: Picks an instance randomly. + + +ROUND_ROBIN: Iterates over instances over and over again. + + +
    +
    +<literal>Content-Type</literal> Template and Version +The Content-Type template and version are represented by the contentTypeTemplate and +version YAML properties. +If you version your API in the Content-Type header, you do not want to add this header +to each of your requests. Also, if you want to call a new version of the API, you do not +want to roam around your code to bump up the API version. That is why you can provide a +contentTypeTemplate with a special $version placeholder. That placeholder will be filled by the value of the +version YAML property. Consider the following example of a contentTypeTemplate: +application/vnd.newsletter.$version+json +Further consider the following version: +v1 +The combination of contentTypeTemplate and version results in the creation of a +Content-Type header for each request, as follows: +application/vnd.newsletter.v1+json +
    +
    +Default Headers +Default headers are represented by the headers map in YAML. +Sometimes, each call to a dependency requires setting up of some default headers. To not +do that in code, you can set them up in the YAML file, as shown in the following example +headers section: +headers: + Accept: + - text/html + - application/xhtml+xml + Cache-Control: + - no-cache +That headers section results in adding the Accept and Cache-Control headers with +appropriate list of values in your HTTP request. +
    +
    +Required Dependencies +Required dependencies are represented by required property in YAML. +If one of your dependencies is required to be up when your application boots, you can set +the required: true property in the YAML file. +If your application cannot localize the required dependency during boot time, it throws an +exception, and the Spring Context fails to set up. In other words, your application cannot +start if the required dependency is not registered in Zookeeper. +You can read more about Spring Cloud Zookeeper Presence Checker +later in this document. +
    +
    +Stubs +You can provide a colon-separated path to the JAR containing stubs of the dependency, as +shown in the following example: +stubs: org.springframework:myApp:stubs +where: + + +org.springframework is the groupId. + + +myApp is the artifactId. + + +stubs is the classifier. (Note that stubs is the default value.) + + +Because stubs is the default classifier, the preceding example is equal to the following +example: +stubs: org.springframework:myApp +
    +
    +
    +Configuring Spring Cloud Zookeeper Dependencies +You can set the following properties to enable or disable parts of Zookeeper Dependencies +functionalities: + + +spring.cloud.zookeeper.dependencies: If you do not set this property, you cannot use +Zookeeper Dependencies. + + +spring.cloud.zookeeper.dependency.ribbon.enabled (enabled by default): Ribbon requires +either explicit global configuration or a particular one for a dependency. By turning on +this property, runtime load balancing strategy resolution is possible, and you can use the +loadBalancerType section of the Zookeeper Dependencies. The configuration that needs +this property has an implementation of LoadBalancerClient that delegates to the +ILoadBalancer presented in the next bullet. + + +spring.cloud.zookeeper.dependency.ribbon.loadbalancer (enabled by default): Thanks to +this property, the custom ILoadBalancer knows that the part of the URI passed to Ribbon +might actually be the alias that has to be resolved to a proper path in Zookeeper. Without +this property, you cannot register applications under nested paths. + + +spring.cloud.zookeeper.dependency.headers.enabled (enabled by default): This property +registers a RibbonClient that automatically appends appropriate headers and content +types with their versions, as presented in the Dependency configuration. Without this +setting, those two parameters do not work. + + +spring.cloud.zookeeper.dependency.resttemplate.enabled (enabled by default): When +enabled, this property modifies the request headers of a @LoadBalanced-annotated +RestTemplate such that it passes headers and content type with the version set in +dependency configuration. Without this setting, those two parameters do not work. + + +
    +
    + +Spring Cloud Zookeeper Dependency Watcher +The Dependency Watcher mechanism lets you register listeners to your dependencies. The +functionality is, in fact, an implementation of the Observator pattern. When a +dependency changes, its state (to either UP or DOWN), some custom logic can be applied. +
    +Activating +Spring Cloud Zookeeper Dependencies functionality needs to be enabled for you to use the +Dependency Watcher mechanism. +
    +
    +Registering a Listener +To register a listener, you must implement an interface called +org.springframework.cloud.zookeeper.discovery.watcher.DependencyWatcherListener and +register it as a bean. The interface gives you one method: +void stateChanged(String dependencyName, DependencyState newState); +If you want to register a listener for a particular dependency, the dependencyName would +be the discriminator for your concrete implementation. newState provides you with +information about whether your dependency has changed to CONNECTED or DISCONNECTED. +
    +
    +Using the Presence Checker +Bound with the Dependency Watcher is the functionality called Presence Checker. It lets +you provide custom behavior when your application boots, to react according to the state +of your dependencies. +The default implementation of the abstract +org.springframework.cloud.zookeeper.discovery.watcher.presence.DependencyPresenceOnStartupVerifier +class is the +org.springframework.cloud.zookeeper.discovery.watcher.presence.DefaultDependencyPresenceOnStartupVerifier, +which works in the following way. + + +If the dependency is marked us required and is not in Zookeeper, when your application +boots, it throws an exception and shuts down. + + +If the dependency is not required, the +org.springframework.cloud.zookeeper.discovery.watcher.presence.LogMissingDependencyChecker +logs that the dependency is missing at the WARN level. + + +Because the DefaultDependencyPresenceOnStartupVerifier is registered only when there is +no bean of type DependencyPresenceOnStartupVerifier, this functionality can be +overridden. +
    +
    + +Distributed Configuration with Zookeeper +Zookeeper provides a +hierarchical namespace +that lets clients store arbitrary data, such as configuration data. Spring Cloud Zookeeper +Config is an alternative to the +Config Server and Client. +Configuration is loaded into the Spring Environment during the special bootstrap +phase. Configuration is stored in the /config namespace by default. Multiple +PropertySource instances are created, based on the application’s name and the active +profiles, to mimic the Spring Cloud Config order of resolving properties. For example, an +application with a name of testApp and with the dev profile has the following property +sources created for it: + + +config/testApp,dev + + +config/testApp + + +config/application,dev + + +config/application + + +The most specific property source is at the top, with the least specific at the bottom. +Properties in the config/application namespace apply to all applications that use +zookeeper for configuration. Properties in the config/testApp namespace are available +only to the instances of the service named testApp. +Configuration is currently read on startup of the application. Sending a HTTP POST +request to /refresh causes the configuration to be reloaded. Watching the configuration +namespace (which Zookeeper supports) is not currently implemented. +
    +Activating +Including a dependency on +org.springframework.cloud:spring-cloud-starter-zookeeper-config enables +autoconfiguration that sets up Spring Cloud Zookeeper Config. + +When working with version 3.4 of Zookeeper you need to change +the way you include the dependency as described here. + +
    +
    +Customizing +Zookeeper Config may be customized by setting the following properties: + +bootstrap.yml + +spring: + cloud: + zookeeper: + config: + enabled: true + root: configuration + defaultContext: apps + profileSeparator: '::' + + + + +enabled: Setting this value to false disables Zookeeper Config. + + +root: Sets the base namespace for configuration values. + + +defaultContext: Sets the name used by all applications. + + +profileSeparator: Sets the value of the separator used to separate the profile name in +property sources with profiles. + + +
    +
    +Access Control Lists (ACLs) +You can add authentication information for Zookeeper ACLs by calling the addAuthInfo +method of a CuratorFramework bean. One way to accomplish this is to provide your own +CuratorFramework bean, as shown in the following example: +@BoostrapConfiguration +public class CustomCuratorFrameworkConfig { + + @Bean + public CuratorFramework curatorFramework() { + CuratorFramework curator = new CuratorFramework(); + curator.addAuthInfo("digest", "user:password".getBytes()); + return curator; + } + +} +Consult +the ZookeeperAutoConfiguration class +to see how the CuratorFramework bean’s default configuration. +Alternatively, you can add your credentials from a class that depends on the existing +CuratorFramework bean, as shown in the following example: +@BoostrapConfiguration +public class DefaultCuratorFrameworkConfig { + + public ZookeeperConfig(CuratorFramework curator) { + curator.addAuthInfo("digest", "user:password".getBytes()); + } + +} +The creation of this bean must occur during the boostrapping phase. You can register +configuration classes to run during this phase by annotating them with +@BootstrapConfiguration and including them in a comma-separated list that you set as the +value of the org.springframework.cloud.bootstrap.BootstrapConfiguration property in the +resources/META-INF/spring.factories file, as shown in the following example: + +resources/META-INF/spring.factories + +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +my.project.CustomCuratorFrameworkConfig,\ +my.project.DefaultCuratorFrameworkConfig + + +
    +
    +
    + +Spring Cloud Security + +Spring Cloud Security offers a set of primitives for building secure +applications and services with minimum fuss. A declarative model which +can be heavily configured externally (or centrally) lends itself to +the implementation of large systems of co-operating, remote components, +usually with a central indentity management service. It is also extremely +easy to use in a service platform like Cloud Foundry. Building on +Spring Boot and Spring Security OAuth2 we can quickly create systems that +implement common patterns like single sign on, token relay and token +exchange. + +Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github. + + + +Quickstart +
    +OAuth2 Single Sign On +Here’s a Spring Cloud "Hello World" app with HTTP Basic +authentication and a single user account: + +app.groovy + +@Grab('spring-boot-starter-security') +@Controller +class Application { + + @RequestMapping('/') + String home() { + 'Hello World' + } + +} + + +You can run it with spring run app.groovy and watch the logs for the password (username is "user"). So far this is just the default for a Spring Boot app. +Here’s a Spring Cloud app with OAuth2 SSO: + +app.groovy + +@Controller +@EnableOAuth2Sso +class Application { + + @RequestMapping('/') + String home() { + 'Hello World' + } + +} + + +Spot the difference? This app will actually behave exactly the same as +the previous one, because it doesn’t know it’s OAuth2 credentals +yet. +You can register an app in github quite easily, so try that if you +want a production app on your own domain. If you are happy to test on +localhost:8080, then set up these properties in your application +configuration: + +application.yml + +security: + oauth2: + client: + clientId: bd1c0a783ccdd1c9b9e4 + clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1 + accessTokenUri: https://github.com/login/oauth/access_token + userAuthorizationUri: https://github.com/login/oauth/authorize + clientAuthenticationScheme: form + resource: + userInfoUri: https://api.github.com/user + preferTokenInfo: false + + +run the app above and it will redirect to github for authorization. If +you are already signed into github you won’t even notice that it has +authenticated. These credentials will only work if your app is +running on port 8080. +To limit the scope that the client asks for when it obtains an access token +you can set security.oauth2.client.scope (comma separated or an array in YAML). By +default the scope is empty and it is up to to Authorization Server to +decide what the defaults should be, usually depending on the settings in +the client registration that it holds. + +The examples above are all Groovy scripts. If you want to write the +same code in Java (or Groovy) you need to add Spring Security OAuth2 +to the classpath (e.g. see the +sample here). + +
    +
    +OAuth2 Protected Resource +You want to protect an API resource with an OAuth2 token? Here’s a +simple example (paired with the client above): + +app.groovy + +@Grab('spring-cloud-starter-security') +@RestController +@EnableResourceServer +class Application { + + @RequestMapping('/') + def home() { + [message: 'Hello World'] + } + +} + + +and + +application.yml + +security: + oauth2: + resource: + userInfoUri: https://api.github.com/user + preferTokenInfo: false + + +
    +
    + +More Detail +
    +Single Sign On + +All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +Spring Boot user guide. + +
    +
    +Token Relay +A Token Relay is where an OAuth2 consumer acts as a Client and +forwards the incoming token to outgoing resource requests. The +consumer can be a pure Client (like an SSO application) or a Resource +Server. +
    +Client Token Relay in Spring Cloud Gateway +If your app also has a +Spring +Cloud Gateway embedded reverse proxy then you +can ask it to forward OAuth2 access tokens downstream to the services +it is proxying. Thus the SSO app above can be enhanced simply like +this: + +App.java + +@Autowired +private TokenRelayGatewayFilterFactory filterFactory; + +@Bean +public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("resource", r -> r.path("/resource") + .filters(f -> f.filter(filterFactory.apply())) + .uri("http://localhost:9000")) + .build(); +} + + +or this + +application.yaml + +spring: + cloud: + gateway: + routes: + - id: resource + uri: http://localhost:9000 + predicates: + - Path=/resource + filters: + - TokenRelay= + + +and it will (in addition to logging the user in and grabbing a token) +pass the authentication token downstream to the services (in this case +/resource). +To enable this for Spring Cloud Gateway add the following dependencies + + +org.springframework.boot:spring-boot-starter-oauth2-client + + +org.springframework.cloud:spring-cloud-starter-security + + +How does it work? The +filter +extracts an access token from the currently authenticated user, +and puts it in a request header for the downstream requests. +For a full working sample see this project. + +The default implementation of ReactiveOAuth2AuthorizedClientService used by TokenRelayGatewayFilterFactory +uses an in-memory data store. You will need to provide your own implementation ReactiveOAuth2AuthorizedClientService +if you need a more robust solution. + +
    +
    +Client Token Relay +If your app is a user facing OAuth2 client (i.e. has declared +@EnableOAuth2Sso or @EnableOAuth2Client) then it has an +OAuth2ClientContext in request scope from Spring Boot. You can +create your own OAuth2RestTemplate from this context and an +autowired OAuth2ProtectedResourceDetails, and then the context will +always forward the access token downstream, also refreshing the access +token automatically if it expires. (These are features of Spring +Security and Spring Boot.) + +Spring Boot (1.4.1) does not create an +OAuth2ProtectedResourceDetails automatically if you are using +client_credentials tokens. In that case you need to create your own +ClientCredentialsResourceDetails and configure it with +@ConfigurationProperties("security.oauth2.client"). + +
    +
    +Client Token Relay in Zuul Proxy +If your app also has a +Spring +Cloud Zuul embedded reverse proxy (using @EnableZuulProxy) then you +can ask it to forward OAuth2 access tokens downstream to the services +it is proxying. Thus the SSO app above can be enhanced simply like +this: + +app.groovy + +@Controller +@EnableOAuth2Sso +@EnableZuulProxy +class Application { + +} + + +and it will (in addition to logging the user in and grabbing a token) +pass the authentication token downstream to the /proxy/* +services. If those services are implemented with +@EnableResourceServer then they will get a valid token in the +correct header. +How does it work? The @EnableOAuth2Sso annotation pulls in +spring-cloud-starter-security (which you could do manually in a +traditional app), and that in turn triggers some autoconfiguration for +a ZuulFilter, which itself is activated because Zuul is on the +classpath (via @EnableZuulProxy). The +filter +just extracts an access token from the currently authenticated user, +and puts it in a request header for the downstream requests. + +Spring Boot does not create an OAuth2RestOperations automatically which is needed for refresh_token. In that case you need to create your own +OAuth2RestOperations so OAuth2TokenRelayFilter can refresh the token if needed. + +
    +
    +Resource Server Token Relay +If your app has @EnableResourceServer you might want to relay the +incoming token downstream to other services. If you use a +RestTemplate to contact the downstream services then this is just a +matter of how to create the template with the right context. +If your service uses UserInfoTokenServices to authenticate incoming +tokens (i.e. it is using the security.oauth2.user-info-uri +configuration), then you can simply create an OAuth2RestTemplate +using an autowired OAuth2ClientContext (it will be populated by the +authentication process before it hits the backend code). Equivalently +(with Spring Boot 1.4), you could inject a +UserInfoRestTemplateFactory and grab its OAuth2RestTemplate in +your configuration. For example: + +MyConfiguration.java + +@Bean +public OAuth2RestTemplate restTemplate(UserInfoRestTemplateFactory factory) { + return factory.getUserInfoRestTemplate(); +} + + +This rest template will then have the same OAuth2ClientContext +(request-scoped) that is used by the authentication filter, so you can +use it to send requests with the same access token. +If your app is not using UserInfoTokenServices but is still a client +(i.e. it declares @EnableOAuth2Client or @EnableOAuth2Sso), then +with Spring Security Cloud any OAuth2RestOperations that the user +creates from an @Autowired OAuth2Context will also forward +tokens. This feature is implemented by default as an MVC handler +interceptor, so it only works in Spring MVC. If you are not using MVC +you could use a custom filter or AOP interceptor wrapping an +AccessTokenContextRelay to provide the same feature. +Here’s a basic +example showing the use of an autowired rest template created +elsewhere ("foo.com" is a Resource Server accepting the same tokens as +the surrounding app): + +MyController.java + +@Autowired +private OAuth2RestOperations restTemplate; + +@RequestMapping("/relay") +public String relay() { + ResponseEntity<String> response = + restTemplate.getForEntity("https://foo.com/bar", String.class); + return "Success! (" + response.getBody() + ")"; +} + + +If you don’t want to forward tokens (and that is a valid +choice, since you might want to act as yourself, rather than the +client that sent you the token), then you only need to create your own +OAuth2Context instead of autowiring the default one. +Feign clients will also pick up an interceptor that uses the +OAuth2ClientContext if it is available, so they should also do a +token relay anywhere where a RestTemplate would. +
    +
    +
    + +Configuring Authentication Downstream of a Zuul Proxy +You can control the authorization behaviour downstream of an +@EnableZuulProxy through the proxy.auth.* settings. Example: + +application.yml + +proxy: + auth: + routes: + customers: oauth2 + stores: passthru + recommendations: none + + +In this example the "customers" service gets an OAuth2 token relay, +the "stores" service gets a passthrough (the authorization header is +just passed downstream), and the "recommendations" service has its +authorization header removed. The default behaviour is to do a token +relay if there is a token available, and passthru otherwise. +See + +ProxyAuthenticationProperties for full details. + +
    + +Spring Cloud for Cloud Foundry + +Spring Cloud for Cloudfoundry makes it easy to run +Spring Cloud apps in +Cloud Foundry (the Platform as a +Service). Cloud Foundry has the notion of a "service", which is +middlware that you "bind" to an app, essentially providing it with an +environment variable containing credentials (e.g. the location and +username to use for the service). +The spring-cloud-cloudfoundry-commons module configures the +Reactor-based Cloud Foundry Java client, v 3.0, and can be used standalone. +The spring-cloud-cloudfoundry-web project provides basic support for +some enhanced features of webapps in Cloud Foundry: binding +automatically to single-sign-on services and optionally enabling +sticky routing for discovery. +The spring-cloud-cloudfoundry-discovery project provides an +implementation of Spring Cloud Commons DiscoveryClient so you can +@EnableDiscoveryClient and provide your credentials as +spring.cloud.cloudfoundry.discovery.[username,password] (also *.url if you are not connecting to Pivotal Web Services) and then you +can use the DiscoveryClient directly or via a LoadBalancerClient. +The first time you use it the discovery client might be slow owing to +the fact that it has to get an access token from Cloud Foundry. + + +Discovery +Here’s a Spring Cloud app with Cloud Foundry discovery: + +app.groovy + +@Grab('org.springframework.cloud:spring-cloud-cloudfoundry') +@RestController +@EnableDiscoveryClient +class Application { + + @Autowired + DiscoveryClient client + + @RequestMapping('/') + String home() { + 'Hello from ' + client.getLocalServiceInstance() + } + +} + + +If you run it without any service bindings: +$ spring jar app.jar app.groovy +$ cf push -p app.jar +It will show its app name in the home page. +The DiscoveryClient can lists all the apps in a space, according to +the credentials it is authenticated with, where the space defaults to +the one the client is running in (if any). If neither org nor space +are configured, they default per the user’s profile in Cloud Foundry. + + +Single Sign On + +All of the OAuth2 SSO and resource server features moved to Spring Boot +in version 1.3. You can find documentation in the +Spring Boot user guide. + +This project provides automatic binding from CloudFoundry service +credentials to the Spring Boot features. If you have a CloudFoundry +service called "sso", for instance, with credentials containing +"client_id", "client_secret" and "auth_domain", it will bind +automatically to the Spring OAuth2 client that you enable with +@EnableOAuth2Sso (from Spring Boot). The name of the service can be +parameterized using spring.oauth2.sso.serviceId. + + + +Spring Cloud Contract + +Documentation Authors: Adam Dudczak, Mathias Düsterhöft, Marcin Grzejszczak, Dennis Kieselhorst, Jakub Kubryński, Karol Lassak, +Olga Maciaszek-Sharma, Mariusz Smykuła, Dave Syer, Jay Bryant +Greenwich.SR5 + + +Spring Cloud Contract +You need confidence when pushing new features to a new application or service in a +distributed system. This project provides support for Consumer Driven Contracts and +service schemas in Spring applications (for both HTTP and message-based interactions), +covering a range of options for writing tests, publishing them as assets, and asserting +that a contract is kept by producers and consumers. + + +Spring Cloud Contract Verifier Introduction +Spring Cloud Contract Verifier enables Consumer Driven Contract (CDC) development of +JVM-based applications. It moves TDD to the level of software architecture. +Spring Cloud Contract Verifier ships with Contract Definition Language (CDL). Contract +definitions are used to produce the following resources: + + +JSON stub definitions to be used by WireMock when doing integration testing on the +client code (client tests). Test code must still be written by hand, and test data is +produced by Spring Cloud Contract Verifier. + + +Messaging routes, if you’re using a messaging service. We integrate with Spring +Integration, Spring Cloud Stream, Spring AMQP, and Apache Camel. You can also set your +own integrations. + + +Acceptance tests (in JUnit 4, JUnit 5 or Spock) are used to verify if server-side implementation +of the API is compliant with the contract (server tests). A full test is generated by +Spring Cloud Contract Verifier. + + +
    +History +Before becoming Spring Cloud Contract, this project was called Accurest. +It was created by Marcin Grzejszczak and Jakub Kubrynski +from (Codearte. +The 0.1.0 release took place on 26 Jan 2015 and it became stable with 1.0.0 release on 29 Feb 2016. +
    +
    +Why a Contract Verifier? +Assume that we have a system consisting of multiple microservices: + + + + + +Microservices Architecture + + +
    +Testing issues +If we wanted to test the application in top left corner to determine whether it can +communicate with other services, we could do one of two things: + + +Deploy all microservices and perform end-to-end tests. + + +Mock other microservices in unit/integration tests. + + +Both have their advantages but also a lot of disadvantages. +Deploy all microservices and perform end to end tests +Advantages: + + +Simulates production. + + +Tests real communication between services. + + +Disadvantages: + + +To test one microservice, we have to deploy 6 microservices, a couple of databases, +etc. + + +The environment where the tests run is locked for a single suite of tests (nobody else +would be able to run the tests in the meantime). + + +They take a long time to run. + + +The feedback comes very late in the process. + + +They are extremely hard to debug. + + +Mock other microservices in unit/integration tests +Advantages: + + +They provide very fast feedback. + + +They have no infrastructure requirements. + + +Disadvantages: + + +The implementor of the service creates stubs that might have nothing to do with +reality. + + +You can go to production with passing tests and failing production. + + +To solve the aforementioned issues, Spring Cloud Contract Verifier with Stub Runner was +created. The main idea is to give you very fast feedback, without the need to set up the +whole world of microservices. If you work on stubs, then the only applications you need +are those that your application directly uses. + + + + + +Stubbed Services + + +Spring Cloud Contract Verifier gives you the certainty that the stubs that you use were +created by the service that you’re calling. Also, if you can use them, it means that they +were tested against the producer’s side. In short, you can trust those stubs. +
    +
    +
    +Purposes +The main purposes of Spring Cloud Contract Verifier with Stub Runner are: + + +To ensure that WireMock/Messaging stubs (used when developing the client) do exactly +what the actual server-side implementation does. + + +To promote ATDD method and Microservices architectural style. + + +To provide a way to publish changes in contracts that are immediately visible on both +sides. + + +To generate boilerplate test code to be used on the server side. + + + +Spring Cloud Contract Verifier’s purpose is NOT to start writing business +features in the contracts. Assume that we have a business use case of fraud check. If a +user can be a fraud for 100 different reasons, we would assume that you would create 2 +contracts, one for the positive case and one for the negative case. Contract tests are +used to test contracts between applications and not to simulate full behavior. + +
    +
    +How It Works +This section explores how Spring Cloud Contract Verifier with Stub Runner works. +
    +A Three-second Tour +This very brief tour walks through using Spring Cloud Contract: + + + + + + + + +You can find a somewhat longer tour +here. +
    +On the Producer Side +To start working with Spring Cloud Contract, add files with REST/ messaging contracts +expressed in either Groovy DSL or YAML to the contracts directory, which is set by the +contractsDslDir property. By default, it is $rootDir/src/test/resources/contracts. +Then add the Spring Cloud Contract Verifier dependency and plugin to your build file, as +shown in the following example: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-verifier</artifactId> + <scope>test</scope> +</dependency> +The following listing shows how to add the plugin, which should go in the build/plugins +portion of the file: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> +</plugin> +Running ./mvnw clean install automatically generates tests that verify the application +compliance with the added contracts. By default, the tests get generated under +org.springframework.cloud.contract.verifier.tests.. +As the implementation of the functionalities described by the contracts is not yet +present, the tests fail. +To make them pass, you must add the correct implementation of either handling HTTP +requests or messages. Also, you must add a correct base test class for auto-generated +tests to the project. This class is extended by all the auto-generated tests, and it +should contain all the setup necessary to run them (for example RestAssuredMockMvc +controller setup or messaging test setup). +Once the implementation and the test base class are in place, the tests pass, and both the +application and the stub artifacts are built and installed in the local Maven repository. +The changes can now be merged, and both the application and the stub artifacts may be +published in an online repository. +
    +
    +On the Consumer Side +Spring Cloud Contract Stub Runner can be used in the integration tests to get a running +WireMock instance or messaging route that simulates the actual service. +To do so, add the dependency to Spring Cloud Contract Stub Runner, as shown in the +following example: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> + <scope>test</scope> +</dependency> +You can get the Producer-side stubs installed in your Maven repository in either of two +ways: + + +By checking out the Producer side repository and adding contracts and generating the stubs +by running the following commands: +$ cd local-http-server-repo +$ ./mvnw clean install -DskipTests + +The tests are being skipped because the Producer-side contract implementation is not +in place yet, so the automatically-generated contract tests fail. + + + +By getting already-existing producer service stubs from a remote repository. To do so, +pass the stub artifact IDs and artifact repository URL as Spring Cloud Contract +Stub Runner properties, as shown in the following example: +stubrunner: + ids: 'com.example:http-server-dsl:+:stubs:8080' + repositoryRoot: https://repo.spring.io/libs-snapshot + + +Now you can annotate your test class with @AutoConfigureStubRunner. In the annotation, +provide the group-id and artifact-id values for Spring Cloud Contract Stub Runner to +run the collaborators' stubs for you, as shown in the following example: +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, + stubsMode = StubRunnerProperties.StubsMode.LOCAL) +public class LoanApplicationServiceTests { + +Use the REMOTE stubsMode when downloading stubs from an online repository and +LOCAL for offline work. + +Now, in your integration test, you can receive stubbed versions of HTTP responses or +messages that are expected to be emitted by the collaborator service. +
    +
    +
    +A Three-minute Tour +This brief tour walks through using Spring Cloud Contract: + + + + + + + + +You can find an even more brief tour +here. +
    +On the Producer Side +To start working with Spring Cloud Contract, add files with REST/ messaging contracts +expressed in either Groovy DSL or YAML to the contracts directory, which is set by the +contractsDslDir property. By default, it is $rootDir/src/test/resources/contracts. +For the HTTP stubs, a contract defines what kind of response should be returned for a +given request (taking into account the HTTP methods, URLs, headers, status codes, and so +on). The following example shows how an HTTP stub contract in Groovy DSL: +package contracts + +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body([ + "client.id": $(regex('[0-9]{10}')), + loanAmount: 99999 + ]) + headers { + contentType('application/json') + } + } + response { + status OK() + body([ + fraudCheckStatus: "FRAUD", + "rejection.reason": "Amount too high" + ]) + headers { + contentType('application/json') + } + } +} +The same contract expressed in YAML would look like the following example: +request: + method: PUT + url: /fraudcheck + body: + "client.id": 1234567890 + loanAmount: 99999 + headers: + Content-Type: application/json + matchers: + body: + - path: $.['client.id'] + type: by_regex + value: "[0-9]{10}" +response: + status: 200 + body: + fraudCheckStatus: "FRAUD" + "rejection.reason": "Amount too high" + headers: + Content-Type: application/json;charset=UTF-8 +In the case of messaging, you can define: + + +The input and the output messages can be defined (taking into account from and where it +was sent, the message body, and the header). + + +The methods that should be called after the message is received. + + +The methods that, when called, should trigger a message. + + +The following example shows a Camel messaging contract expressed in Groovy DSL: + def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } +The following example shows the same contract expressed in YAML: +label: some_label +input: + messageFrom: jms:delete + messageBody: + bookName: 'foo' + messageHeaders: + sample: header + assertThat: bookWasDeleted() +Then you can add Spring Cloud Contract Verifier dependency and plugin to your build file, +as shown in the following example: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-verifier</artifactId> + <scope>test</scope> +</dependency> +The following listing shows how to add the plugin, which should go in the build/plugins +portion of the file: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> +</plugin> +Running ./mvnw clean install automatically generates tests that verify the application +compliance with the added contracts. By default, the generated tests are under +org.springframework.cloud.contract.verifier.tests.. +The following example shows a sample auto-generated test for an HTTP contract: +@Test +public void validate_shouldMarkClientAsFraud() throws Exception { + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/vnd.fraud.v1+json") + .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}"); + + // when: + ResponseOptions response = given().spec(request) + .put("/fraudcheck"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}"); + assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high"); +} +The preceding example uses Spring’s MockMvc to run the tests. This is the default test +mode for HTTP contracts. However, JAX-RS client and explicit HTTP invocations can also be +used. (To do so, change the testMode property of the plugin to JAX-RS or EXPLICIT, +respectively.) +Since 2.1.0, it is also possible to use RestAssuredWebTestClient`with Spring’s reactive `WebTestClient +run under the hood. This is particularly recommended while working with Reactive, Web-Flux-based applications. +In order to use WebTestClient set testMode to WEBTESTCLIENT. +Here is an example of a test generated in WEBTESTCLIENT test mode: +[source,java,indent=0] +@Test + public void validate_shouldRejectABeerIfTooYoung() throws Exception { + // given: + WebTestClientRequestSpecification request = given() + .header("Content-Type", "application/json") + .body("{\"age\":10}"); + + // when: + WebTestClientResponse response = given().spec(request) + .post("/check"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/json.*"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK"); + } +Apart from the default JUnit 4, you can instead use JUnit 5 or Spock tests, by setting the plugin +testFramework property to either JUNIT5 or Spock. + +You can now also generate WireMock scenarios based on the contracts, by including an +order number followed by an underscore at the beginning of the contract file names. + +The following example shows an auto-generated test in Spock for a messaging stub contract: +[source,groovy,indent=0] +given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + \'\'\'{"bookName":"foo"}\'\'\', + ['sample': 'header'] + ) + +when: + contractVerifierMessaging.send(inputMessage, 'jms:delete') + +then: + noExceptionThrown() + bookWasDeleted() +As the implementation of the functionalities described by the contracts is not yet +present, the tests fail. +To make them pass, you must add the correct implementation of handling either HTTP +requests or messages. Also, you must add a correct base test class for auto-generated +tests to the project. This class is extended by all the auto-generated tests and should +contain all the setup necessary to run them (for example, RestAssuredMockMvc controller +setup or messaging test setup). +Once the implementation and the test base class are in place, the tests pass, and both the +application and the stub artifacts are built and installed in the local Maven repository. +Information about installing the stubs jar to the local repository appears in the logs, as +shown in the following example: +[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server --- +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar +[INFO] +[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server --- +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar +[INFO] +[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server --- +[INFO] +[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server --- +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar +[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar +You can now merge the changes and publish both the application and the stub artifacts +in an online repository. +Docker Project +In order to enable working with contracts while creating applications in non-JVM +technologies, the springcloud/spring-cloud-contract Docker image has been created. It +contains a project that automatically generates tests for HTTP contracts and executes them +in EXPLICIT test mode. Then, if the tests pass, it generates Wiremock stubs and, +optionally, publishes them to an artifact manager. In order to use the image, you can +mount the contracts into the /contracts directory and set a few environment variables. +
    +
    +On the Consumer Side +Spring Cloud Contract Stub Runner can be used in the integration tests to get a running +WireMock instance or messaging route that simulates the actual service. +To get started, add the dependency to Spring Cloud Contract Stub Runner: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> + <scope>test</scope> +</dependency> +You can get the Producer-side stubs installed in your Maven repository in either of two +ways: + + +By checking out the Producer side repository and adding contracts and generating the +stubs by running the following commands: +$ cd local-http-server-repo +$ ./mvnw clean install -DskipTests + +The tests are skipped because the Producer-side contract implementation is not yet +in place, so the automatically-generated contract tests fail. + + + +Getting already existing producer service stubs from a remote repository. To do so, +pass the stub artifact IDs and artifact repository URl as Spring Cloud Contract Stub +Runner properties, as shown in the following example: +stubrunner: + ids: 'com.example:http-server-dsl:+:stubs:8080' + repositoryRoot: https://repo.spring.io/libs-snapshot + + +Now you can annotate your test class with @AutoConfigureStubRunner. In the annotation, +provide the group-id and artifact-id for Spring Cloud Contract Stub Runner to run +the collaborators' stubs for you, as shown in the following example: +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"}, + stubsMode = StubRunnerProperties.StubsMode.LOCAL) +public class LoanApplicationServiceTests { + +Use the REMOTE stubsMode when downloading stubs from an online repository and +LOCAL for offline work. + +In your integration test, you can receive stubbed versions of HTTP responses or messages +that are expected to be emitted by the collaborator service. You can see entries similar +to the following in the build logs: +2016-07-19 14:22:25.403 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is + - will try to resolve the latest version +2016-07-19 14:22:25.438 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is 0.0.1-SNAPSHOT +2016-07-19 14:22:25.439 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories [] +2016-07-19 14:22:25.451 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar +2016-07-19 14:22:25.465 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar] +2016-07-19 14:22:25.475 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265] +2016-07-19 14:22:27.737 INFO 41050 --- [ main] o.s.c.c.stubrunner.StubRunnerExecutor : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}] +
    +
    +
    +Defining the Contract +As consumers of services, we need to define what exactly we want to achieve. We need to +formulate our expectations. That is why we write contracts. +Assume that you want to send a request containing the ID of a client company and the +amount it wants to borrow from us. You also want to send it to the /fraudcheck url via +the PUT method. + +Groovy DSL + +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package contracts + +org.springframework.cloud.contract.spec.Contract.make { + request { // (1) + method 'PUT' // (2) + url '/fraudcheck' // (3) + body([ // (4) + "client.id": $(regex('[0-9]{10}')), + loanAmount : 99999 + ]) + headers { // (5) + contentType('application/json') + } + } + response { // (6) + status OK() // (7) + body([ // (8) + fraudCheckStatus : "FRAUD", + "rejection.reason": "Amount too high" + ]) + headers { // (9) + contentType('application/json') + } + } +} + +/* +From the Consumer perspective, when shooting a request in the integration test: + +(1) - If the consumer sends a request +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `client.id` that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/json` +(6) - then the response will be sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` equal to `application/json` + +From the Producer perspective, in the autogenerated producer-side test: + +(1) - A request will be sent to the producer +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/json` +(6) - then the test will assert if the response has been sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` matching `application/json.*` + */ + + + +YAML + +request: # (1) + method: PUT # (2) + url: /fraudcheck # (3) + body: # (4) + "client.id": 1234567890 + loanAmount: 99999 + headers: # (5) + Content-Type: application/json + matchers: + body: + - path: $.['client.id'] # (6) + type: by_regex + value: "[0-9]{10}" +response: # (7) + status: 200 # (8) + body: # (9) + fraudCheckStatus: "FRAUD" + "rejection.reason": "Amount too high" + headers: # (10) + Content-Type: application/json;charset=UTF-8 + + +#From the Consumer perspective, when shooting a request in the integration test: +# +#(1) - If the consumer sends a request +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `client.id` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}` +#(7) - then the response will be sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json` +# +#From the Producer perspective, in the autogenerated producer-side test: +# +#(1) - A request will be sent to the producer +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `client.id` `1234567890` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(7) - then the test will assert if the response has been sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8` + + +
    +
    +Client Side +Spring Cloud Contract generates stubs, which you can use during client-side testing. +You get a running WireMock instance/Messaging route that simulates the service. +You would like to feed that instance with a proper stub definition. +At some point in time, you need to send a request to the Fraud Detection service. +ResponseEntity<FraudServiceResponse> response = restTemplate.exchange( + "http://localhost:" + port + "/fraudcheck", HttpMethod.PUT, + new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class); +Annotate your test class with @AutoConfigureStubRunner. In the annotation provide the group id and artifact id for the Stub Runner to download stubs of your collaborators. +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = { + "com.example:http-server-dsl:+:stubs:6565" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL) +public class LoanApplicationServiceTests { +After that, during the tests, Spring Cloud Contract automatically finds the stubs +(simulating the real service) in the Maven repository and exposes them on a configured +(or random) port. +
    +
    +Server Side +Since you are developing your stub, you need to be sure that it actually resembles your +concrete implementation. You cannot have a situation where your stub acts in one way and +your application behaves in a different way, especially in production. +To ensure that your application behaves the way you define in your stub, tests are +generated from the stub you provide. +The autogenerated test looks, more or less, like this: +@Test +public void validate_shouldMarkClientAsFraud() throws Exception { + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/vnd.fraud.v1+json") + .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}"); + + // when: + ResponseOptions response = given().spec(request) + .put("/fraudcheck"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}"); + assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high"); +} +
    +
    +
    +Step-by-step Guide to Consumer Driven Contracts (CDC) +Consider an example of Fraud Detection and the Loan Issuance process. The business +scenario is such that we want to issue loans to people but do not want them to steal from +us. The current implementation of our system grants loans to everybody. +Assume that Loan Issuance is a client to the Fraud Detection server. In the current +sprint, we must develop a new feature: if a client wants to borrow too much money, then +we mark the client as a fraud. +Technical remark - Fraud Detection has an artifact-id of http-server, while Loan +Issuance has an artifact-id of http-client, and both have a group-id of com.example. +Social remark - both client and server development teams need to communicate directly and +discuss changes while going through the process. CDC is all about communication. +The server +side code is available here and the +client code here. + +In this case, the producer owns the contracts. Physically, all the contract are +in the producer’s repository. + +
    +Technical note +If using the SNAPSHOT / Milestone / Release Candidate versions please add the +following section to your build: + +Maven + +<repositories> + <repository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </repository> + <repository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + <repository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> +</repositories> +<pluginRepositories> + <pluginRepository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> +</pluginRepositories> + + + +Gradle + +repositories { + mavenCentral() + mavenLocal() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + maven { url "https://repo.spring.io/release" } +} + + +
    +
    +Consumer side (Loan Issuance) +As a developer of the Loan Issuance service (a consumer of the Fraud Detection server), you might do the following steps: + + +Start doing TDD by writing a test for your feature. + + +Write the missing implementation. + + +Clone the Fraud Detection service repository locally. + + +Define the contract locally in the repo of Fraud Detection service. + + +Add the Spring Cloud Contract Verifier plugin. + + +Run the integration tests. + + +File a pull request. + + +Create an initial implementation. + + +Take over the pull request. + + +Write the missing implementation. + + +Deploy your app. + + +Work online. + + +Start doing TDD by writing a test for your feature. +@Test +public void shouldBeRejectedDueToAbnormalLoanAmount() { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 99999); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED); + assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high"); +} +Assume that you have written a test of your new feature. If a loan application for a big +amount is received, the system should reject that loan application with some description. +Write the missing implementation. +At some point in time, you need to send a request to the Fraud Detection service. Assume +that you need to send the request containing the ID of the client and the amount the +client wants to borrow. You want to send it to the /fraudcheck url via the PUT method. +ResponseEntity<FraudServiceResponse> response = restTemplate.exchange( + "http://localhost:" + port + "/fraudcheck", HttpMethod.PUT, + new HttpEntity<>(request, httpHeaders), FraudServiceResponse.class); +For simplicity, the port of the Fraud Detection service is set to 8080, and the +application runs on 8090. +If you start the test at this point, it breaks, because no service currently runs on port +8080. +Clone the Fraud Detection service repository locally. +You can start by playing around with the server side contract. To do so, you must first +clone it. +$ git clone https://your-git-server.com/server-side.git local-http-server-repo +Define the contract locally in the repo of Fraud Detection service. +As a consumer, you need to define what exactly you want to achieve. You need to formulate +your expectations. To do so, write the following contract: + +Place the contract under src/test/resources/contracts/fraud folder. The fraud folder +is important because the producer’s test base class name references that folder. + + +Groovy DSL + +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package contracts + +org.springframework.cloud.contract.spec.Contract.make { + request { // (1) + method 'PUT' // (2) + url '/fraudcheck' // (3) + body([ // (4) + "client.id": $(regex('[0-9]{10}')), + loanAmount : 99999 + ]) + headers { // (5) + contentType('application/json') + } + } + response { // (6) + status OK() // (7) + body([ // (8) + fraudCheckStatus : "FRAUD", + "rejection.reason": "Amount too high" + ]) + headers { // (9) + contentType('application/json') + } + } +} + +/* +From the Consumer perspective, when shooting a request in the integration test: + +(1) - If the consumer sends a request +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `client.id` that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/json` +(6) - then the response will be sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` equal to `application/json` + +From the Producer perspective, in the autogenerated producer-side test: + +(1) - A request will be sent to the producer +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/json` +(6) - then the test will assert if the response has been sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` matching `application/json.*` + */ + + + +YAML + +request: # (1) + method: PUT # (2) + url: /fraudcheck # (3) + body: # (4) + "client.id": 1234567890 + loanAmount: 99999 + headers: # (5) + Content-Type: application/json + matchers: + body: + - path: $.['client.id'] # (6) + type: by_regex + value: "[0-9]{10}" +response: # (7) + status: 200 # (8) + body: # (9) + fraudCheckStatus: "FRAUD" + "rejection.reason": "Amount too high" + headers: # (10) + Content-Type: application/json;charset=UTF-8 + + +#From the Consumer perspective, when shooting a request in the integration test: +# +#(1) - If the consumer sends a request +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `client.id` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}` +#(7) - then the response will be sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json` +# +#From the Producer perspective, in the autogenerated producer-side test: +# +#(1) - A request will be sent to the producer +#(2) - With the "PUT" method +#(3) - to the URL "/fraudcheck" +#(4) - with the JSON body that +# * has a field `client.id` `1234567890` +# * has a field `loanAmount` that is equal to `99999` +#(5) - with header `Content-Type` equal to `application/json` +#(7) - then the test will assert if the response has been sent with +#(8) - status equal `200` +#(9) - and JSON body equal to +# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +#(10) - with header `Content-Type` equal to `application/json;charset=UTF-8` + + +The YML contract is quite straight-forward. However when you take a look at the Contract +written using a statically typed Groovy DSL - you might wonder what the +value(client(…​), server(…​)) parts are. By using this notation, Spring Cloud +Contract lets you define parts of a JSON block, a URL, etc., which are dynamic. In case +of an identifier or a timestamp, you need not hardcode a value. You want to allow some +different ranges of values. To enable ranges of values, you can set regular expressions +matching those values for the consumer side. You can provide the body by means of either +a map notation or String with interpolations. +Consult the section for more information. We highly recommend using the map notation! + +You must understand the map notation in order to set up contracts. Please read the +Groovy docs regarding JSON. + +The previously shown contract is an agreement between two sides that: + + +if an HTTP request is sent with all of + + +a PUT method on the /fraudcheck endpoint, + + +a JSON body with a client.id that matches the regular expression [0-9]{10} and +loanAmount equal to 99999, + + +and a Content-Type header with a value of application/vnd.fraud.v1+json, + + + + +then an HTTP response is sent to the consumer that + + +has status 200, + + +contains a JSON body with the fraudCheckStatus field containing a value FRAUD and +the rejectionReason field having value Amount too high, + + +and a Content-Type header with a value of application/vnd.fraud.v1+json. + + + + +Once you are ready to check the API in practice in the integration tests, you need to +install the stubs locally. +Add the Spring Cloud Contract Verifier plugin. +We can add either a Maven or a Gradle plugin. In this example, you see how to add Maven. +First, add the Spring Cloud Contract BOM. +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${spring-cloud-release.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> +Next, add the Spring Cloud Contract Verifier Maven plugin +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example.fraud</packageWithBaseClasses> + <convertToYaml>true</convertToYaml> + </configuration> +</plugin> +Since the plugin was added, you get the Spring Cloud Contract Verifier features which, +from the provided contracts: + + +generate and run tests + + +produce and install stubs + + +You do not want to generate tests since you, as the consumer, want only to play with the +stubs. You need to skip the test generation and execution. When you execute: +$ cd local-http-server-repo +$ ./mvnw clean install -DskipTests +In the logs, you see something like this: +[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server --- +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar +[INFO] +[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server --- +[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar +[INFO] +[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server --- +[INFO] +[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server --- +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar +[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar +The following line is extremely important: +[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar +It confirms that the stubs of the http-server have been installed in the local +repository. +Run the integration tests. +In order to profit from the Spring Cloud Contract Stub Runner functionality of automatic +stub downloading, you must do the following in your consumer side project (Loan +Application service): +Add the Spring Cloud Contract BOM: +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${spring-cloud-release-train.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> +Add the dependency to Spring Cloud Contract Stub Runner: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> + <scope>test</scope> +</dependency> +Annotate your test class with @AutoConfigureStubRunner. In the annotation, provide the +group-id and artifact-id for the Stub Runner to download the stubs of your +collaborators. (Optional step) Because you’re playing with the collaborators offline, you +can also provide the offline work switch (StubRunnerProperties.StubsMode.LOCAL). +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = { + "com.example:http-server-dsl:+:stubs:6565" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL) +public class LoanApplicationServiceTests { +Now, when you run your tests, you see something like this: +2016-07-19 14:22:25.403 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is + - will try to resolve the latest version +2016-07-19 14:22:25.438 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is 0.0.1-SNAPSHOT +2016-07-19 14:22:25.439 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories [] +2016-07-19 14:22:25.451 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar +2016-07-19 14:22:25.465 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar] +2016-07-19 14:22:25.475 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265] +2016-07-19 14:22:27.737 INFO 41050 --- [ main] o.s.c.c.stubrunner.StubRunnerExecutor : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}] +This output means that Stub Runner has found your stubs and started a server for your app +with group id com.example, artifact id http-server with version 0.0.1-SNAPSHOT of +the stubs and with stubs classifier on port 8080. +File a pull request. +What you have done until now is an iterative process. You can play around with the +contract, install it locally, and work on the consumer side until the contract works as +you wish. +Once you are satisfied with the results and the test passes, publish a pull request to +the server side. Currently, the consumer side work is done. +
    +
    +Producer side (Fraud Detection server) +As a developer of the Fraud Detection server (a server to the Loan Issuance service): +Create an initial implementation. +As a reminder, you can see the initial implementation here: +@RequestMapping(value = "/fraudcheck", method = PUT) +public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) { +return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON); +} +Take over the pull request. +$ git checkout -b contract-change-pr master +$ git pull https://your-git-server.com/server-side-fork.git contract-change-pr +You must add the dependencies needed by the autogenerated tests: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-verifier</artifactId> + <scope>test</scope> +</dependency> +In the configuration of the Maven plugin, pass the packageWithBaseClasses property +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example.fraud</packageWithBaseClasses> + <convertToYaml>true</convertToYaml> + </configuration> +</plugin> + +This example uses "convention based" naming by setting the +packageWithBaseClasses property. Doing so means that the two last packages combine to +make the name of the base test class. In our case, the contracts were placed under +src/test/resources/contracts/fraud. Since you do not have two packages starting from +the contracts folder, pick only one, which should be fraud. Add the Base suffix and +capitalize fraud. That gives you the FraudBase test class name. + +All the generated tests extend that class. Over there, you can set up your Spring Context +or whatever is necessary. In this case, use Rest Assured MVC to +start the server side FraudDetectionController. +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fraud; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; + +public class FraudBase { + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(), + new FraudStatsController(stubbedStatsProvider())); + } + + private StatsProvider stubbedStatsProvider() { + return fraudType -> { + switch (fraudType) { + case DRUNKS: + return 100; + case ALL: + return 200; + } + return 0; + }; + } + + public void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } + +} +Now, if you run the ./mvnw clean install, you get something like this: +Results : + +Tests in error: + ContractVerifierTest.validate_shouldMarkClientAsFraud:32 » IllegalState Parsed... +This error occurs because you have a new contract from which a test was generated and it +failed since you have not implemented the feature. The auto-generated test would look +like this: +@Test +public void validate_shouldMarkClientAsFraud() throws Exception { + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/vnd.fraud.v1+json") + .body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}"); + + // when: + ResponseOptions response = given().spec(request) + .put("/fraudcheck"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}"); + assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high"); +} +If you used the Groovy DSL, you can see, all the producer() parts of the Contract that were present in the +value(consumer(…​), producer(…​)) blocks got injected into the test. +In case of using YAML, the same applied for the matchers sections of the response. +Note that, on the producer side, you are also doing TDD. The expectations are expressed +in the form of a test. This test sends a request to our own application with the URL, +headers, and body defined in the contract. It also is expecting precisely defined values +in the response. In other words, you have the red part of red, green, and +refactor. It is time to convert the red into the green. +Write the missing implementation. +Because you know the expected input and expected output, you can write the missing +implementation: +@RequestMapping(value = "/fraudcheck", method = PUT) +public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) { +if (amountGreaterThanThreshold(fraudCheck)) { + return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH); +} +return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON); +} +When you execute ./mvnw clean install again, the tests pass. Since the Spring Cloud +Contract Verifier plugin adds the tests to the generated-test-sources, you can +actually run those tests from your IDE. +Deploy your app. +Once you finish your work, you can deploy your change. First, merge the branch: +$ git checkout master +$ git merge --no-ff contract-change-pr +$ git push origin master +Your CI might run something like ./mvnw clean deploy, which would publish both the +application and the stub artifacts. +
    +
    +Consumer Side (Loan Issuance) Final Step +As a developer of the Loan Issuance service (a consumer of the Fraud Detection server): +Merge branch to master. +$ git checkout master +$ git merge --no-ff contract-change-pr +Work online. +Now you can disable the offline work for Spring Cloud Contract Stub Runner and indicate +where the repository with your stubs is located. At this moment the stubs of the server +side are automatically downloaded from Nexus/Artifactory. You can set the value of +stubsMode to REMOTE. The following code shows an example of +achieving the same thing by changing the properties. +stubrunner: + ids: 'com.example:http-server-dsl:+:stubs:8080' + repositoryRoot: https://repo.spring.io/libs-snapshot +That’s it! +
    +
    +
    +Dependencies +The best way to add dependencies is to use the proper starter dependency. +For stub-runner, use spring-cloud-starter-stub-runner. When you use a plugin, add +spring-cloud-starter-contract-verifier. +
    +
    +Additional Links +Here are some resources related to Spring Cloud Contract Verifier and Stub Runner. Note +that some may be outdated, because the Spring Cloud Contract Verifier project is under +constant development. +
    +Spring Cloud Contract video +You can check out the video from the Warsaw JUG about Spring Cloud Contract: + +
    +
    +Readings + + +Slides from Marcin Grzejszczak’s talk about Accurest + + +Accurest related articles from Marcin Grzejszczak’s blog + + +Spring Cloud Contract related articles from Marcin Grzejszczak’s blog + + +Groovy docs regarding JSON + + +
    +
    +
    +Samples +You can find some samples at +samples. +
    +
    + +Spring Cloud Contract FAQ +
    +Why use Spring Cloud Contract Verifier and not X ? +For the time being Spring Cloud Contract is a JVM based tool. So it could be your first pick when you’re already creating +software for the JVM. This project has a lot of really interesting features but especially quite a few of them definitely make +Spring Cloud Contract Verifier stand out on the "market" of Consumer Driven Contract (CDC) tooling. Out of many the most interesting are: + + +Possibility to do CDC with messaging + + +Clear and easy to use, statically typed DSL + + +Possibility to copy paste your current JSON file to the contract and only edit its elements + + +Automatic generation of tests from the defined Contract + + +Stub Runner functionality - the stubs are automatically downloaded at runtime from Nexus / Artifactory + + +Spring Cloud integration - no discovery service is needed for integration tests + + +Spring Cloud Contract integrates with Pact out of the box and provides easy hooks to extend its functionality + + +Via Docker adds support for any language & framework used + + +
    +
    +I don’t want to write a contract in Groovy! +No problem. You can write a contract in YAML! +
    +
    +What is this value(consumer(), producer()) ? +One of the biggest challenges related to stubs is their reusability. Only if they can be vastly used, will they serve their purpose. +What typically makes that difficult are the hard-coded values of request / response elements. For example dates or ids. +Imagine the following JSON request +{ + "time" : "2016-10-10 20:10:15", + "id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a", + "body" : "foo" +} +and JSON response +{ + "time" : "2016-10-10 21:10:15", + "id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051", + "body" : "bar" +} +Imagine the pain required to set proper value of the time field (let’s assume that this content is generated by the +database) by changing the clock in the system or providing stub implementations of data providers. The same is related +to the field called id. Will you create a stubbed implementation of UUID generator? Makes little sense…​ +So as a consumer you would like to send a request that matches any form of a time or any UUID. That way your system +will work as usual - will generate data and you won’t have to stub anything out. Let’s assume that in case of the aforementioned +JSON the most important part is the body field. You can focus on that and provide matching for other fields. In other words +you would like the stub to work like this: +{ + "time" : "SOMETHING THAT MATCHES TIME", + "id" : "SOMETHING THAT MATCHES UUID", + "body" : "foo" +} +As far as the response goes as a consumer you need a concrete value that you can operate on. So such a JSON is valid +{ + "time" : "2016-10-10 21:10:15", + "id" : "c4231e1f-3ca9-48d3-b7e7-567d55f0d051", + "body" : "bar" +} +As you could see in the previous sections we generate tests from contracts. So from the producer’s side the situation looks +much different. We’re parsing the provided contract and in the test we want to send a real request to your endpoints. +So for the case of a producer for the request we can’t have any sort of matching. We need concrete values that the +producer’s backend can work on. Such a JSON would be a valid one: +{ + "time" : "2016-10-10 20:10:15", + "id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a", + "body" : "foo" +} +On the other hand from the point of view of the validity of the contract the response doesn’t necessarily have to +contain concrete values of time or id. Let’s say that you generate those on the producer side - again, you’d +have to do a lot of stubbing to ensure that you always return the same values. That’s why from the producer’s side +what you might want is the following response: +{ + "time" : "SOMETHING THAT MATCHES TIME", + "id" : "SOMETHING THAT MATCHES UUID", + "body" : "bar" +} +How can you then provide one time a matcher for the consumer and a concrete value for the producer and vice versa? +In Spring Cloud Contract we’re allowing you to provide a dynamic value. That means that it can differ for both +sides of the communication. You can pass the values: +Either via the value method +value(consumer(...), producer(...)) +value(stub(...), test(...)) +value(client(...), server(...)) +or using the $() method +$(consumer(...), producer(...)) +$(stub(...), test(...)) +$(client(...), server(...)) +You can read more about this in the section. +Calling value() or $() tells Spring Cloud Contract that you will be passing a dynamic value. +Inside the consumer() method you pass the value that should be used on the consumer side (in the generated stub). +Inside the producer() method you pass the value that should be used on the producer side (in the generated test). + +If on one side you have passed the regular expression and you haven’t passed the other, then the +other side will get auto-generated. + +Most often you will use that method together with the regex helper method. E.g. consumer(regex('[0-9]{10}')). +To sum it up the contract for the aforementioned scenario would look more or less like this (the regular expression +for time and UUID are simplified and most likely invalid but we want to keep things very simple in this example): +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url '/someUrl' + body([ + time : value(consumer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')), + id: value(consumer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}')) + body: "foo" + ]) + } + response { + status OK() + body([ + time : value(producer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')), + id: value([producer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}')) + body: "bar" + ]) + } +} + +Please read the Groovy docs related to JSON to understand how to +properly structure the request / response bodies. + +
    +
    +How to do Stubs versioning? +
    +API Versioning +Let’s try to answer a question what versioning really means. If you’re referring to the API version then there are +different approaches. + + +use Hypermedia, links and do not version your API by any means + + +pass versions through headers / urls + + +I will not try to answer a question which approach is better. Whatever suits your needs and allows you to generate +business value should be picked. +Let’s assume that you do version your API. In that case you should provide as many contracts as many versions you support. +You can create a subfolder for every version or append it to the contract name - whatever suits you more. +
    +
    +JAR versioning +If by versioning you mean the version of the JAR that contains the stubs then there are essentially two main approaches. +Let’s assume that you’re doing Continuous Delivery / Deployment which means that you’re generating a new version of +the jar each time you go through the pipeline and that jar can go to production at any time. For example your jar version +looks like this (it got built on the 20.10.2016 at 20:15:21) : +1.0.0.20161020-201521-RELEASE +In that case your generated stub jar will look like this. +1.0.0.20161020-201521-RELEASE-stubs.jar +In this case you should inside your application.yml or @AutoConfigureStubRunner when referencing stubs provide the + latest version of the stubs. You can do that by passing the + sign. Example +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"}) +If the versioning however is fixed (e.g. 1.0.4.RELEASE or 2.1.1) then you have to set the concrete value of the jar +version. Example for 2.1.1. +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:2.1.1:stubs:8080"}) +
    +
    +Dev or prod stubs +You can manipulate the classifier to run the tests against current development version of the stubs of other services + or the ones that were deployed to production. If you alter your build to deploy the stubs with the prod-stubs classifier + once you reach production deployment then you can run tests in one case with dev stubs and one with prod stubs. +Example of tests using development version of stubs +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:8080"}) +Example of tests using production version of stubs +@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:prod-stubs:8080"}) +You can pass those values also via properties from your deployment pipeline. +
    +
    +
    +Common repo with contracts +Another way of storing contracts other than having them with the producer is keeping them in a common place. +It can be related to security issues where the consumers can’t clone the producer’s code. Also if you keep +contracts in a single place then you, as a producer, will know how many consumers you have and which +consumer you will break with your local changes. +
    +Repo structure +Let’s assume that we have a producer with coordinates com.example:server and 3 consumers: client1, +client2, client3. Then in the repository with common contracts you would have the following setup +(which you can checkout here): +├── com +│   └── example +│   └── server +│   ├── client1 +│   │   └── expectation.groovy +│   ├── client2 +│   │   └── expectation.groovy +│   ├── client3 +│   │   └── expectation.groovy +│   └── pom.xml +├── mvnw +├── mvnw.cmd +├── pom.xml +└── src + └── assembly + └── contracts.xml +As you can see under the slash-delimited groupid / artifact id folder (com/example/server) you have +expectations of the 3 consumers (client1, client2 and client3). Expectations are the standard Groovy DSL +contract files as described throughout this documentation. This repository has to produce a JAR file that maps +one to one to the contents of the repo. +Example of a pom.xml inside the server folder. +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>com.example</groupId> + <artifactId>server</artifactId> + <version>0.0.1-SNAPSHOT</version> + + <name>Server Stubs</name> + <description>POM used to install locally stubs for consumer side</description> + + <parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>2.1.10.RELEASE</version> + <relativePath/> + </parent> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <java.version>1.8</java.version> + <spring-cloud-contract.version>2.1.6.BUILD-SNAPSHOT</spring-cloud-contract.version> + <spring-cloud-release.version>Greenwich.BUILD-SNAPSHOT + </spring-cloud-release.version> + <excludeBuildFolders>true</excludeBuildFolders> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${spring-cloud-release.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <plugins> + <plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <!-- By default it would search under src/test/resources/ --> + <contractsDirectory>${project.basedir}</contractsDirectory> + </configuration> + </plugin> + </plugins> + </build> + + <repositories> + <repository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </repository> + <repository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + <repository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + </repositories> + <pluginRepositories> + <pluginRepository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> + </pluginRepositories> + +</project> +As you can see there are no dependencies other than the Spring Cloud Contract Maven Plugin. +Those poms are necessary for the consumer side to run mvn clean install -DskipTests to locally install + stubs of the producer project. +The pom.xml in the root folder can look like this: +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>com.example.standalone</groupId> + <artifactId>contracts</artifactId> + <version>0.0.1-SNAPSHOT</version> + + <name>Contracts</name> + <description>Contains all the Spring Cloud Contracts, well, contracts. JAR used by the + producers to generate tests and stubs + </description> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <id>contracts</id> + <phase>prepare-package</phase> + <goals> + <goal>single</goal> + </goals> + <configuration> + <attach>true</attach> + <descriptor>${basedir}/src/assembly/contracts.xml</descriptor> + <!-- If you want an explicit classifier remove the following line --> + <appendAssemblyId>false</appendAssemblyId> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> + +</project> +It’s using the assembly plugin in order to build the JAR with all the contracts. Example of such setup is here: +<assembly xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd"> + <id>project</id> + <formats> + <format>jar</format> + </formats> + <includeBaseDirectory>false</includeBaseDirectory> + <fileSets> + <fileSet> + <directory>${project.basedir}</directory> + <outputDirectory>/</outputDirectory> + <useDefaultExcludes>true</useDefaultExcludes> + <excludes> + <exclude>**/${project.build.directory}/**</exclude> + <exclude>mvnw</exclude> + <exclude>mvnw.cmd</exclude> + <exclude>.mvn/**</exclude> + <exclude>src/**</exclude> + </excludes> + </fileSet> + </fileSets> +</assembly> +
    +
    +Workflow +The workflow would look similar to the one presented in the Step by step guide to CDC. The only difference + is that the producer doesn’t own the contracts anymore. So the consumer and the producer have to work on + common contracts in a common repository. +
    +
    +Consumer +When the consumer wants to work on the contracts offline, instead of cloning the producer code, the +consumer team clones the common repository, goes to the required producer’s folder (e.g. com/example/server) +and runs mvn clean install -DskipTests to install locally the stubs converted from the contracts. + +You need to have Maven installed locally + +
    +
    +Producer +As a producer it’s enough to alter the Spring Cloud Contract Verifier to provide the URL and the dependency +of the JAR containing the contracts: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <configuration> + <contractsMode>REMOTE</contractsMode> + <contractsRepositoryUrl> + https://link/to/your/nexus/or/artifactory/or/sth + </contractsRepositoryUrl> + <contractDependency> + <groupId>com.example.standalone</groupId> + <artifactId>contracts</artifactId> + </contractDependency> + </configuration> +</plugin> +With this setup the JAR with groupid com.example.standalone and artifactid contracts will be downloaded +from http://link/to/your/nexus/or/artifactory/or/sth. It will be then unpacked in a local temporary folder +and contracts present under the com/example/server will be picked as the ones used to generate the +tests and the stubs. Due to this convention the producer team will know which consumer teams will be broken +when some incompatible changes are done. +The rest of the flow looks the same. +
    +
    +How can I define messaging contracts per topic not per producer? +To avoid messaging contracts duplication in the common repo, when few producers writing messages to one topic, +we could create the structure when the rest contracts would be placed in a folder per producer and messaging +contracts in the folder per topic. +
    +For Maven Project +To make it possible to work on the producer side we should specify an inclusion pattern for +filtering common repository jar by messaging topics we are interested in. includedFiles property of Maven Spring Cloud Contract plugin +allows us to do that. Also contractsPath need to be specified since the default path would be the common repository groupid/artifactid. +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <configuration> + <contractsMode>REMOTE</contractsMode> + <contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl> + <contractDependency> + <groupId>com.example</groupId> + <artifactId>common-repo-with-contracts</artifactId> + <version>+</version> + </contractDependency> + <contractsPath>/</contractsPath> + <baseClassMappings> + <baseClassMapping> + <contractPackageRegex>.*messaging.*</contractPackageRegex> + <baseClassFQN>com.example.services.MessagingBase</baseClassFQN> + </baseClassMapping> + <baseClassMapping> + <contractPackageRegex>.*rest.*</contractPackageRegex> + <baseClassFQN>com.example.services.TestBase</baseClassFQN> + </baseClassMapping> + </baseClassMappings> + <includedFiles> + <includedFile>**/${project.artifactId}/**</includedFile> + <includedFile>**/${first-topic}/**</includedFile> + <includedFile>**/${second-topic}/**</includedFile> + </includedFiles> + </configuration> +</plugin> +
    +
    +For Gradle Project + + +Add a custom configuration for the common-repo dependency: + + +ext { + conractsGroupId = "com.example" + contractsArtifactId = "common-repo" + contractsVersion = "1.2.3" +} + +configurations { + contracts { + transitive = false + } +} + + +Add the common-repo dependency to your classpath: + + +dependencies { + contracts "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}" + testCompile "${conractsGroupId}:${contractsArtifactId}:${contractsVersion}" +} + + +Download the dependency to an appropriate folder: + + +task getContracts(type: Copy) { + from configurations.contracts + into new File(project.buildDir, "downloadedContracts") +} + + +Unzip JAR: + + +task unzipContracts(type: Copy) { + def zipFile = new File(project.buildDir, "downloadedContracts/${contractsArtifactId}-${contractsVersion}.jar") + def outputDir = file("${buildDir}/unpackedContracts") + + from zipTree(zipFile) + into outputDir +} + + +Cleanup unused contracts: + + +task deleteUnwantedContracts(type: Delete) { + delete fileTree(dir: "${buildDir}/unpackedContracts", + include: "**/*", + excludes: [ + "**/${project.name}/**"", + "**/${first-topic}/**", + "**/${second-topic}/**"]) +} + + +Create task dependencies: + + +unzipContracts.dependsOn("getContracts") +deleteUnwantedContracts.dependsOn("unzipContracts") +build.dependsOn("deleteUnwantedContracts") + + +Configure plugin by specifying the directory containing contracts using contractsDslDir property + + +contracts { + contractsDslDir = new File("${buildDir}/unpackedContracts") +} +
    +
    +
    +
    +Do I need a Binary Storage? Can’t I use Git? +In the polyglot world, there are languages that don’t use binary storages like +Artifactory or Nexus. Starting from Spring Cloud Contract version 2.0.0 we provide +mechanisms to store contracts and stubs in a SCM repository. Currently the +only supported SCM is Git. +The repository would have to the following setup +(which you can checkout here): +. +└── META-INF + └── com.example + └── beer-api-producer-git + └── 0.0.1-SNAPSHOT + ├── contracts + │   └── beer-api-consumer + │   ├── messaging + │   │   ├── shouldSendAcceptedVerification.groovy + │   │   └── shouldSendRejectedVerification.groovy + │   └── rest + │   ├── shouldGrantABeerIfOldEnough.groovy + │   └── shouldRejectABeerIfTooYoung.groovy + └── mappings + └── beer-api-consumer + └── rest + ├── shouldGrantABeerIfOldEnough.json + └── shouldRejectABeerIfTooYoung.json +Under META-INF folder: + + +we group applications via groupId (e.g. com.example) + + +then each application is represented via the artifactId (e.g. beer-api-producer-git) + + +next, the version of the application (e.g. 0.0.1-SNAPSHOT). Starting from Spring Cloud Contract version 2.1.0, you can specify the versions as follows (assuming that your versions follow the semantic versioning) + + ++ or latest - to find the latest version of your stubs (assuming that the snapshots are always the latest artifact for a given revision number). That means: + + +if you have a version 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT and 2.0.0.RELEASE we will assume that the latest is 2.0.0.BUILD-SNAPSHOT + + +if you have a version 1.0.0.RELEASE and 2.0.0.RELEASE we will assume that the latest is 2.0.0.RELEASE + + +if you have a version called latest or + we will pick that folder + + + + +release - to find the latest release version of your stubs. That means: + + +if you have a version 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT and 2.0.0.RELEASE we will assume that the latest is 2.0.0.RELEASE + + +if you have a version called release we will pick that folder + + + + + + +finally, there are two folders: + + +contracts - the good practice is to store the contracts required by each +consumer in the folder with the consumer name (e.g. beer-api-consumer). That way you +can use the stubs-per-consumer feature. Further directory structure is arbitrary. + + +mappings - in this folder the Maven / Gradle Spring Cloud Contract plugins will push +the stub server mappings. On the consumer side, Stub Runner will scan this folder +to start stub servers with stub definitions. The folder structure will be a copy +of the one created in the contracts subfolder. + + + + +
    +Protocol convention +In order to control the type and location of the source of contracts (whether it’s +a binary storage or an SCM repository), you can use the protocol in the URL of +the repository. Spring Cloud Contract iterates over registered protocol resolvers +and tries to fetch the contracts (via a plugin) or stubs (via Stub Runner). +For the SCM functionality, currently, we support the Git repository. To use it, +in the property, where the repository URL needs to be placed you just have to prefix +the connection URL with git://. Here you can find a couple of examples: +git://file:///foo/bar +git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git +git://git@github.com:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git +
    +
    +Producer +For the producer, to use the SCM approach, we can reuse the +same mechanism we use for external contracts. We route Spring Cloud Contract +to use the SCM implementation via the URL that contains +the git:// protocol. + +You have to manually add the pushStubsToScm +goal in Maven or execute (bind) the pushStubsToScm task in +Gradle. We don’t push stubs to origin of your git +repository out of the box. + + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <!-- Base class mappings etc. --> + + <!-- We want to pick contracts from a Git repository --> + <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl> + + <!-- We reuse the contract dependency section to set up the path + to the folder that contains the contract definitions. In our case the + path will be /groupId/artifactId/version/contracts --> + <contractDependency> + <groupId>${project.groupId}</groupId> + <artifactId>${project.artifactId}</artifactId> + <version>${project.version}</version> + </contractDependency> + + <!-- The contracts mode can't be classpath --> + <contractsMode>REMOTE</contractsMode> + </configuration> + <executions> + <execution> + <phase>package</phase> + <goals> + <!-- By default we will not push the stubs back to SCM, + you have to explicitly add it as a goal --> + <goal>pushStubsToScm</goal> + </goals> + </execution> + </executions> +</plugin> + + + +Gradle + +contracts { + // We want to pick contracts from a Git repository + contractDependency { + stringNotation = "${project.group}:${project.name}:${project.version}" + } + /* + We reuse the contract dependency section to set up the path + to the folder that contains the contract definitions. In our case the + path will be /groupId/artifactId/version/contracts + */ + contractRepository { + repositoryUrl = "git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git" + } + // The mode can't be classpath + contractsMode = "REMOTE" + // Base class mappings etc. +} + +/* +In this scenario we want to publish stubs to SCM whenever +the `publish` task is executed +*/ +publish.dependsOn("publishStubsToScm") + + +With such a setup: + + +Git project will be cloned to a temporary directory + + +The SCM stub downloader will go to META-INF/groupId/artifactId/version/contracts folder +to find contracts. E.g. for com.example:foo:1.0.0 the path would be +META-INF/com.example/foo/1.0.0/contracts + + +Tests will be generated from the contracts + + +Stubs will be created from the contracts + + +Once the tests pass, the stubs will be committed in the cloned repository + + +Finally, a push will be done to that repo’s origin + + +
    +
    +Producer with contracts stored locally +Another option to use the SCM as the destination for stubs and contracts is to store the contracts locally, with the producer, and only push the contracts and the stubs to SCM. Below, you can find the setup required to achieve this using Maven and Gradle. + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <!-- In the default configuration, we want to use the contracts stored locally --> + <configuration> + <baseClassMappings> + <baseClassMapping> + <contractPackageRegex>.*messaging.*</contractPackageRegex> + <baseClassFQN>com.example.BeerMessagingBase</baseClassFQN> + </baseClassMapping> + <baseClassMapping> + <contractPackageRegex>.*rest.*</contractPackageRegex> + <baseClassFQN>com.example.BeerRestBase</baseClassFQN> + </baseClassMapping> + </baseClassMappings> + <basePackageForTests>com.example</basePackageForTests> + </configuration> + <executions> + <execution> + <phase>package</phase> + <goals> + <!-- By default we will not push the stubs back to SCM, + you have to explicitly add it as a goal --> + <goal>pushStubsToScm</goal> + </goals> + <configuration> + <!-- We want to pick contracts from a Git repository --> + <contractsRepositoryUrl>git://file://${env.ROOT}/target/contract_empty_git/ + </contractsRepositoryUrl> + <!-- Example of URL via git protocol --> + <!--<contractsRepositoryUrl>git://git@github.com:spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>--> + <!-- Example of URL via http protocol --> + <!--<contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-samples.git</contractsRepositoryUrl>--> + <!-- We reuse the contract dependency section to set up the path + to the folder that contains the contract definitions. In our case the + path will be /groupId/artifactId/version/contracts --> + <contractDependency> + <groupId>${project.groupId}</groupId> + <artifactId>${project.artifactId}</artifactId> + <version>${project.version}</version> + </contractDependency> + <!-- The mode can't be classpath --> + <contractsMode>LOCAL</contractsMode> + </configuration> + </execution> + </executions> +</plugin> + + + +Gradle + +contracts { + // Base package for generated tests + basePackageForTests = "com.example" + baseClassMappings { + baseClassMapping(".*messaging.*", "com.example.BeerMessagingBase") + baseClassMapping(".*rest.*", "com.example.BeerRestBase") + } +} + +/* +In this scenario we want to publish stubs to SCM whenever +the `publish` task is executed +*/ +publishStubsToScm { + // We want to modify the default set up of the plugin when publish stubs to scm is called + customize { + // We want to pick contracts from a Git repository + contractDependency { + stringNotation = "${project.group}:${project.name}:${project.version}" + } + /* + We reuse the contract dependency section to set up the path + to the folder that contains the contract definitions. In our case the + path will be /groupId/artifactId/version/contracts + */ + contractRepository { + repositoryUrl = "git://file://${System.getenv("ROOT")}/target/contract_empty_git/" + } + // The mode can't be classpath + contractsMode = "LOCAL" + } +} + +publish.dependsOn("publishStubsToScm") +publishToMavenLocal.dependsOn("publishStubsToScm") + + +With such a setup: + + +Contracts from the default src/test/resources/contracts directory will be picked + + +Tests will be generated from the contracts + + +Stubs will be created from the contracts + + +Once the tests pass + + +Git project will be cloned to a temporary directory + + +The stubs and contracts will be committed in the cloned repository + + + + +Finally, a push will be done to that repo’s origin + + +
    +Keeping contracts with the producer and stubs in an external repository +It is also possible to keep the contracts in the producer repository, but keep the stubs in an external git repo. +This is most useful when you want to use the base consumer-producer collaboration flow, but do not have a possibility to +use an artifact repository for storing the stubs. +In order to do that, use the usual producer setup, and then add the pushStubsToScm goal and set +contractsRepositoryUrl to the repository where you want to keep the stubs. +
    +
    +
    +Consumer +On the consumer side when passing the repositoryRoot parameter, +either from the @AutoConfigureStubRunner annotation, the +JUnit rule, JUnit 5 extension or properties, it’s enough to pass the URL of the +SCM repository, prefixed with the protocol. For example +@AutoConfigureStubRunner( + stubsMode="REMOTE", + repositoryRoot="git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git", + ids="com.example:bookstore:0.0.1.RELEASE" +) +With such a setup: + + +Git project will be cloned to a temporary directory + + +The SCM stub downloader will go to META-INF/groupId/artifactId/version/ folder +to find stub definitions and contracts. E.g. for com.example:foo:1.0.0 the path would be +META-INF/com.example/foo/1.0.0/ + + +Stub servers will be started and fed with mappings + + +Messaging definitions will be read and used in the messaging tests + + +
    +
    +
    +Can I use the Pact Broker? +When using Pact you can use the Pact Broker +to store and share Pact definitions. Starting from Spring Cloud Contract +2.0.0 one can fetch Pact files from the Pact Broker to generate +tests and stubs. +As a prerequisite the Pact Converter and Pact Stub Downloader +are required. You have to add them via the spring-cloud-contract-pact dependency. +You can read more about it in the section. + +Pact follows the Consumer Contract convention. That means +that the Consumer creates the Pact definitions first, then +shares the files with the Producer. Those expectations are generated +from the Consumer’s code and can break the Producer if the expectations +are not met. + +
    +Pact Consumer +The consumer uses Pact framework to generate Pact files. The +Pact files are sent to the Pact Broker. An example of such +setup can be found here. +
    +
    +Producer +For the producer, to use the Pact files from the Pact Broker, we can reuse the +same mechanism we use for external contracts. We route Spring Cloud Contract +to use the Pact implementation via the URL that contains +the pact:// protocol. It’s enough to pass the URL to the +Pact Broker. An example of such setup can be found here. + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <!-- Base class mappings etc. --> + + <!-- We want to pick contracts from a Git repository --> + <contractsRepositoryUrl>pact://http://localhost:8085</contractsRepositoryUrl> + + <!-- We reuse the contract dependency section to set up the path + to the folder that contains the contract definitions. In our case the + path will be /groupId/artifactId/version/contracts --> + <contractDependency> + <groupId>${project.groupId}</groupId> + <artifactId>${project.artifactId}</artifactId> + <!-- When + is passed, a latest tag will be applied when fetching pacts --> + <version>+</version> + </contractDependency> + + <!-- The contracts mode can't be classpath --> + <contractsMode>REMOTE</contractsMode> + </configuration> + <!-- Don't forget to add spring-cloud-contract-pact to the classpath! --> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-pact</artifactId> + <version>${spring-cloud-contract.version}</version> + </dependency> + </dependencies> +</plugin> + + + +Gradle + +buildscript { + repositories { + //... + } + + dependencies { + // ... + // Don't forget to add spring-cloud-contract-pact to the classpath! + classpath "org.springframework.cloud:spring-cloud-contract-pact:${contractVersion}" + } +} + +contracts { + // When + is passed, a latest tag will be applied when fetching pacts + contractDependency { + stringNotation = "${project.group}:${project.name}:+" + } + contractRepository { + repositoryUrl = "pact://http://localhost:8085" + } + // The mode can't be classpath + contractsMode = "REMOTE" + // Base class mappings etc. +} + + +With such a setup: + + +Pact files will be downloaded from the Pact Broker + + +Spring Cloud Contract will convert the Pact files into tests and stubs + + +The JAR with the stubs gets automatically created as usual + + +
    +
    +Pact Consumer (Producer Contract approach) +In the scenario where you don’t want to do Consumer Contract approach +(for every single consumer define the expectations) but you’d prefer +to do Producer Contracts (the producer provides the contracts and +publishes stubs), it’s enough to use Spring Cloud Contract with +Stub Runner option. An example of such setup can be found here. +First, remember to add Stub Runner and Spring Cloud Contract Pact module +as test dependencies. + +Maven + +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${spring-cloud.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> + +<!-- Don't forget to add spring-cloud-contract-pact to the classpath! --> +<dependencies> + <!-- ... --> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-pact</artifactId> + <scope>test</scope> + </dependency> +</dependencies> + + + +Gradle + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +dependencies { + //... + testCompile("org.springframework.cloud:spring-cloud-starter-contract-stub-runner") + // Don't forget to add spring-cloud-contract-pact to the classpath! + testCompile("org.springframework.cloud:spring-cloud-contract-pact") +} + + +Next, just pass the URL of the Pact Broker to repositoryRoot, prefixed +with pact:// protocol. E.g. pact://http://localhost:8085 +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureStubRunner(stubsMode = StubRunnerProperties.StubsMode.REMOTE, + ids = "com.example:beer-api-producer-pact", + repositoryRoot = "pact://http://localhost:8085") +public class BeerControllerTest { + //Inject the port of the running stub + @StubRunnerPort("beer-api-producer-pact") int producerPort; + //... +} +With such a setup: + + +Pact files will be downloaded from the Pact Broker + + +Spring Cloud Contract will convert the Pact files into stub definitions + + +The stub servers will be started and fed with stubs + + +For more information about Pact support you can go to +the section. +
    +
    +
    +How can I debug the request/response being sent by the generated tests client? +The generated tests all boil down to RestAssured in some form or fashion which relies on Apache HttpClient. HttpClient has a facility called wire logging which logs the entire request and response to HttpClient. Spring Boot has a logging common application property for doing this sort of thing, just add this to your application properties +logging.level.org.apache.http.wire=DEBUG +
    +How can I debug the mapping/request/response being sent by WireMock? +Starting from version 1.2.0 we turn on WireMock logging to +info and the WireMock notifier to being verbose. Now you will +exactly know what request was received by WireMock server and which +matching response definition was picked. +To turn off this feature just bump WireMock logging to ERROR +logging.level.com.github.tomakehurst.wiremock=ERROR +
    +
    +How can I see what got registered in the HTTP server stub? +You can use the mappingsOutputFolder property on @AutoConfigureStubRunner, StubRunnerRule or +`StubRunnerExtension`to dump all mappings per artifact id. Also the port at which the given stub server +was started will be attached. +
    +
    +Can I reference text from file? +Yes! With version 1.2.0 we’ve added such a possibility. It’s enough to call file(…​) method in the +DSL and provide a path relative to where the contract lays. +If you’re using YAML just use the bodyFromFile property. +
    +
    +
    + +Spring Cloud Contract Verifier Setup +You can set up Spring Cloud Contract Verifier in the following ways: + + +As a Gradle project + + +As a Maven project + + +As a Docker project + + +
    +Gradle Project +To learn how to set up the Gradle project for Spring Cloud Contract Verifier, read the +following sections: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +Prerequisites +In order to use Spring Cloud Contract Verifier with WireMock, you muse use either a +Gradle or a Maven plugin. + +If you want to use Spock in your projects, you must add separately the +spock-core and spock-spring modules. Check Spock +docs for more information + +
    +
    +
    + +Add Gradle Plugin with Dependencies +To add a Gradle plugin with dependencies, you can use code similar to the following: + + +Plugin DSL GA versions + +// build.gradle +plugins { + id "groovy" + // this will work only for GA versions of Spring Cloud Contract + id "org.springframework.cloud.contract" version "${GAVerifierVersion}" +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${GAVerifierVersion}" + } +} + +dependencies { + testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}" + // example with adding Spock core and Spock Spring + testCompile "org.spockframework:spock-core:${spockVersion}" + testCompile "org.spockframework:spock-spring:${spockVersion}" + testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' +} + + + +Plugin DSL non GA versions + +// settings.gradle +pluginManagement { + plugins { + id "org.springframework.cloud.contract" version "${verifierVersion}" + } + repositories { + // to pick from local .m2 + mavenLocal() + // for snapshots + maven { url "https://repo.spring.io/snapshot" } + // for milestones + maven { url "https://repo.spring.io/milestone" } + // for GA versions + gradlePluginPortal() + } +} + +// build.gradle +plugins { + id "groovy" + id "org.springframework.cloud.contract" +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifierVersion}" + } +} + +dependencies { + testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}" + // example with adding Spock core and Spock Spring + testCompile "org.spockframework:spock-core:${spockVersion}" + testCompile "org.spockframework:spock-spring:${spockVersion}" + testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' +} + + + +Legacy Plugin Application + +// build.gradle +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}" + classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}" + // here you can also pass additional dependencies such as Pact or Kotlin spec e.g.: + // classpath "org.springframework.cloud:spring-cloud-contract-spec-kotlin:${verifier_version}" + } +} + +apply plugin: 'groovy' +apply plugin: 'spring-cloud-contract' + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-contract-dependencies:${verifier_version}" + } +} + +dependencies { + testCompile "org.codehaus.groovy:groovy-all:${groovyVersion}" + // example with adding Spock core and Spock Spring + testCompile "org.spockframework:spock-core:${spockVersion}" + testCompile "org.spockframework:spock-spring:${spockVersion}" + testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' +} + + + +
    +Gradle and Rest Assured 2.0 +By default, Rest Assured 3.x is added to the classpath. However, to use Rest Assured 2.x +you can add it to the plugins classpath, as shown here: +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}" + classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${verifier_version}" + classpath "com.jayway.restassured:rest-assured:2.5.0" + classpath "com.jayway.restassured:spring-mock-mvc:2.5.0" + } +} + +depenendencies { + // all dependencies + // you can exclude rest-assured from spring-cloud-contract-verifier + testCompile "com.jayway.restassured:rest-assured:2.5.0" + testCompile "com.jayway.restassured:spring-mock-mvc:2.5.0" +} +That way, the plugin automatically sees that Rest Assured 2.x is present on the classpath +and modifies the imports accordingly. +
    +
    +Snapshot Versions for Gradle +Add the additional snapshot repository to your build.gradle to use snapshot versions, +which are automatically uploaded after every successful build, as shown here: +/* + We need to use the [buildscript {}] section when we have to modify + the classpath for the plugins. If that's not the case this section + can be skipped. + + If you don't need to modify the classpath (e.g. add a Pact dependency), + then you can just set the [pluginManagement {}] section in [settings.gradle] file. + + // settings.gradle + pluginManagement { + repositories { + // for snapshots + maven {url "https://repo.spring.io/snapshot"} + // for milestones + maven {url "https://repo.spring.io/milestone"} + // for GA versions + gradlePluginPortal() + } + } + + */ +buildscript { + repositories { + mavenCentral() + mavenLocal() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + maven { url "https://repo.spring.io/release" } + } +} +
    +
    +Add stubs +By default, Spring Cloud Contract Verifier is looking for stubs in the +src/test/resources/contracts directory. +The directory containing stub definitions is treated as a class name, and each stub +definition is treated as a single test. Spring Cloud Contract Verifier assumes that it +contains at least one level of directories that are to be used as the test class name. +If more than one level of nested directories is present, all except the last one is used +as the package name. For example, with following structure: +src/test/resources/contracts/myservice/shouldCreateUser.groovy +src/test/resources/contracts/myservice/shouldReturnUser.groovy +Spring Cloud Contract Verifier creates a test class named defaultBasePackage.MyService +with two methods: + + +shouldCreateUser() + + +shouldReturnUser() + + +
    +
    +Run the Plugin +The plugin registers itself to be invoked before a check task. If you want it to be +part of your build process, you need to do nothing more. If you just want to generate +tests, invoke the generateContractTests task. +
    +
    +Default Setup +The default Gradle Plugin setup creates the following Gradle part of the build (in +pseudocode): +contracts { + testFramework ='JUNIT' + testMode = 'MockMvc' + generatedTestSourcesDir = project.file("${project.buildDir}/generated-test-sources/contracts") + generatedTestResourcesDir = project.file("${project.buildDir}/generated-test-resources/contracts") + contractsDslDir = file("${project.rootDir}/src/test/resources/contracts") + basePackageForTests = 'org.springframework.cloud.verifier.tests' + stubsOutputDir = project.file("${project.buildDir}/stubs") + + // the following properties are used when you want to provide where the JAR with contract lays + contractDependency { + stringNotation = '' + } + contractsPath = '' + contractsWorkOffline = false + contractRepository { + cacheDownloadedContracts(true) + } +} + +tasks.create(type: Jar, name: 'verifierStubsJar', dependsOn: 'generateClientStubs') { + baseName = project.name + classifier = contracts.stubsSuffix + from contractVerifier.stubsOutputDir +} + +project.artifacts { + archives task +} + +tasks.create(type: Copy, name: 'copyContracts') { + from contracts.contractsDslDir + into contracts.stubsOutputDir +} + +verifierStubsJar.dependsOn 'copyContracts' + +publishing { + publications { + stubs(MavenPublication) { + artifactId project.name + artifact verifierStubsJar + } + } +} +
    +
    +Configure Plugin +To change the default configuration, add a contracts snippet to your Gradle config, as +shown here: +contracts { + testMode = 'MockMvc' + baseClassForTests = 'org.mycompany.tests' + generatedTestSourcesDir = project.file('src/generatedContract') +} +
    +
    +Configuration Options + + +testMode: Defines the mode for acceptance tests. By default, the mode is MockMvc, +which is based on Spring’s MockMvc. It can also be changed to WebTestClient, JaxRsClient or to +Explicit for real HTTP calls. + + +imports: Creates an array with imports that should be included in generated tests +(for example ['org.myorg.Matchers']). By default, it creates an empty array. + + +staticImports: Creates an array with static imports that should be included in +generated tests(for example ['org.myorg.Matchers.*']). By default, it creates an empty +array. + + +basePackageForTests: Specifies the base package for all generated tests. If not set, +the value is picked from baseClassForTests’s package and from `packageWithBaseClasses. +If neither of these values are set, then the value is set to +org.springframework.cloud.contract.verifier.tests. + + +baseClassForTests: Creates a base class for all generated tests. By default, if you +use Spock classes, the class is spock.lang.Specification. + + +packageWithBaseClasses: Defines a package where all the base classes reside. This +setting takes precedence over baseClassForTests. + + +baseClassMappings: Explicitly maps a contract package to a FQN of a base class. This +setting takes precedence over packageWithBaseClasses and baseClassForTests. + + +ruleClassForTests: Specifies a rule that should be added to the generated test +classes. + + +ignoredFiles: Uses an Antmatcher to allow defining stub files for which processing +should be skipped. By default, it is an empty array. + + +contractsDslDir: Specifies the directory containing contracts written using the +GroovyDSL. By default, its value is $rootDir/src/test/resources/contracts. + + +generatedTestSourcesDir: Specifies the test source directory where tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-sources/contracts. + + +generatedTestResourcesDir: Specifies the test resource directory where resources used by the tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-resources/contracts. + + +stubsOutputDir: Specifies the directory where the generated WireMock stubs from +the Groovy DSL should be placed. + + +testFramework: Specifies the target test framework to be used. Currently, Spock, JUnit 4 (TestFramework.JUNIT) and +JUnit 5 are supported with JUnit 4 being the default framework. + + +contractsProperties: a map containing properties to be passed to Spring Cloud Contract +components. Those properties might be used by e.g. inbuilt or custom Stub Downloaders. + + +The following properties are used when you want to specify the location of the JAR +containing the contracts: + + +contractDependency: Specifies the Dependency that provides +groupid:artifactid:version:classifier coordinates. You can use the contractDependency +closure to set it up. + + +contractsPath: Specifies the path to the jar. If contract dependencies are +downloaded, the path defaults to groupid/artifactid where groupid is slash +separated. Otherwise, it scans contracts under the provided directory. + + +contractsMode: Specifies the mode of downloading contracts (whether the +JAR is available offline, remotely etc.) + + +deleteStubsAfterTest: If set to false will not remove any downloaded +contracts from temporary directories + + +Below you can find a list of experimental features you can turn on via the plugin: + + +convertToYaml: converts all DSLs to the declarative, YAML format. This can be extremely useful when you’re using external libraries in your Groovy DSLs. By turning this feature on (by setting it to true) you will not need to add the library dependency on the consumer side. + + +assertJsonSize: You can check the size of JSON arrays in the generated tests. This feature is disabled by default. + + +
    +
    +Single Base Class for All Tests +When using Spring Cloud Contract Verifier in default MockMvc, you need to create a base +specification for all generated acceptance tests. In this class, you need to point to an +endpoint, which should be verified. +abstract class BaseMockMvcSpec extends Specification { + + def setup() { + RestAssuredMockMvc.standaloneSetup(new PairIdController()) + } + + void isProperCorrelationId(Integer correlationId) { + assert correlationId == 123456 + } + + void isEmpty(String value) { + assert value == null + } + +} +If you use Explicit mode, you can use a base class to initialize the whole tested app +as you might see in regular integration tests. If you use the JAXRSCLIENT mode, this +base class should also contain a protected WebTarget webTarget field. Right now, the +only option to test the JAX-RS API is to start a web server. +
    +
    +Different Base Classes for Contracts +If your base classes differ between contracts, you can tell the Spring Cloud Contract +plugin which class should get extended by the autogenerated tests. You have two options: + + +Follow a convention by providing the packageWithBaseClasses + + +Provide explicit mapping via baseClassMappings + + +By Convention +The convention is such that if you have a contract under (for example) +src/test/resources/contract/foo/bar/baz/ and set the value of the +packageWithBaseClasses property to com.example.base, then Spring Cloud Contract +Verifier assumes that there is a BarBazBase class under the com.example.base package. +In other words, the system takes the last two parts of the package, if they exist, and +forms a class with a Base suffix. This rule takes precedence over baseClassForTests. +Here is an example of how it works in the contracts closure: +packageWithBaseClasses = 'com.example.base' +By Mapping +You can manually map a regular expression of the contract’s package to fully qualified +name of the base class for the matched contract. You have to provide a list called +baseClassMappings that consists baseClassMapping objects that takes a +contractPackageRegex to baseClassFQN mapping. Consider the following example: +baseClassForTests = "com.example.FooBase" +baseClassMappings { + baseClassMapping('.*/com/.*', 'com.example.ComBase') + baseClassMapping('.*/bar/.*': 'com.example.BarBase') +} +Let’s assume that you have contracts under + - src/test/resources/contract/com/ + - src/test/resources/contract/foo/ +By providing the baseClassForTests, we have a fallback in case mapping did not succeed. +(You could also provide the packageWithBaseClasses as a fallback.) That way, the tests +generated from src/test/resources/contract/com/ contracts extend the +com.example.ComBase, whereas the rest of the tests extend com.example.FooBase. +
    +
    +Invoking Generated Tests +To ensure that the provider side is compliant with defined contracts, you need to invoke: +./gradlew generateContractTests test +
    +
    +Pushing stubs to SCM +If you’re using the SCM repository to keep the contracts and +stubs, you might want to automate the step of pushing stubs to +the repository. To do that, it’s enough to call the pushStubsToScm +task. Example: +$ ./gradlew pushStubsToScm +Under you can find all possible +configuration options that you can pass either via +the contractsProperties field e.g. contracts { contractsProperties = [foo:"bar"] }, +via contractsProperties method e.g. contracts { contractsProperties([foo:"bar"]) }, +a system property or an environment variable. +
    +
    +Spring Cloud Contract Verifier on the Consumer Side +In a consuming service, you need to configure the Spring Cloud Contract Verifier plugin +in exactly the same way as in case of provider. If you do not want to use Stub Runner +then you need to copy contracts stored in src/test/resources/contracts and generate +WireMock JSON stubs using: +./gradlew generateClientStubs + +The stubsOutputDir option has to be set for stub generation to work. + +When present, JSON stubs can be used in automated tests of consuming a service. +@ContextConfiguration(loader == SpringApplicationContextLoader, classes == Application) +class LoanApplicationServiceSpec extends Specification { + + @ClassRule + @Shared + WireMockClassRule wireMockRule == new WireMockClassRule() + + @Autowired + LoanApplicationService sut + + def 'should successfully apply for loan'() { + given: + LoanApplication application = + new LoanApplication(client: new Client(clientPesel: '12345678901'), amount: 123.123) + when: + LoanApplicationResult loanApplication == sut.loanApplication(application) + then: + loanApplication.loanApplicationStatus == LoanApplicationStatus.LOAN_APPLIED + loanApplication.rejectionReason == null + } +} +LoanApplication makes a call to FraudDetection service. This request is handled by a +WireMock server configured with stubs generated by Spring Cloud Contract Verifier. +
    +
    +Maven Project +To learn how to set up the Maven project for Spring Cloud Contract Verifier, read the +following sections: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +Add maven plugin +Add the Spring Cloud Contract BOM in a fashion similar to this: +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>${spring-cloud-release.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> +Next, add the Spring Cloud Contract Verifier Maven plugin: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example.fraud</packageWithBaseClasses> + <convertToYaml>true</convertToYaml> + </configuration> +</plugin> +You can read more in the +Spring +Cloud Contract Maven Plugin Documentation (example for 2.0.0.RELEASE version). +
    +
    +Maven and Rest Assured 2.0 +By default, Rest Assured 3.x is added to the classpath. However, you can use Rest +Assured 2.x by adding it to the plugins classpath, as shown here: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example</packageWithBaseClasses> + </configuration> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-verifier</artifactId> + <version>${spring-cloud-contract.version}</version> + </dependency> + <dependency> + <groupId>com.jayway.restassured</groupId> + <artifactId>rest-assured</artifactId> + <version>2.5.0</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.jayway.restassured</groupId> + <artifactId>spring-mock-mvc</artifactId> + <version>2.5.0</version> + <scope>compile</scope> + </dependency> + </dependencies> +</plugin> + +<dependencies> + <!-- all dependencies --> + <!-- you can exclude rest-assured from spring-cloud-contract-verifier --> + <dependency> + <groupId>com.jayway.restassured</groupId> + <artifactId>rest-assured</artifactId> + <version>2.5.0</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.jayway.restassured</groupId> + <artifactId>spring-mock-mvc</artifactId> + <version>2.5.0</version> + <scope>test</scope> + </dependency> +</dependencies> +That way, the plugin automatically sees that Rest Assured 3.x is present on the classpath +and modifies the imports accordingly. +
    +
    +Snapshot versions for Maven +For Snapshot and Milestone versions, you have to add the following section to your +pom.xml, as shown here: +<repositories> + <repository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </repository> + <repository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + <repository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> +</repositories> +<pluginRepositories> + <pluginRepository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> +</pluginRepositories> +
    +
    +Add stubs +By default, Spring Cloud Contract Verifier is looking for stubs in the +src/test/resources/contracts directory. The directory containing stub definitions is +treated as a class name, and each stub definition is treated as a single test. We assume +that it contains at least one directory to be used as test class name. If there is more +than one level of nested directories, all except the last one is used as package name. +For example, with following structure: +src/test/resources/contracts/myservice/shouldCreateUser.groovy +src/test/resources/contracts/myservice/shouldReturnUser.groovy +Spring Cloud Contract Verifier creates a test class named defaultBasePackage.MyService +with two methods + + +shouldCreateUser() + + +shouldReturnUser() + + +
    +
    +Run plugin +The plugin goal generateTests is assigned to be invoked in the phase called +generate-test-sources. If you want it to be part of your build process, you need not do +anything. If you just want to generate tests, invoke the generateTests goal. +
    +
    +Configure plugin +To change the default configuration, just add a configuration section to the plugin +definition or the execution definition, as shown here: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>convert</goal> + <goal>generateStubs</goal> + <goal>generateTests</goal> + </goals> + </execution> + </executions> + <configuration> + <basePackageForTests>org.springframework.cloud.verifier.twitter.place</basePackageForTests> + <baseClassForTests>org.springframework.cloud.verifier.twitter.place.BaseMockMvcSpec</baseClassForTests> + </configuration> +</plugin> +
    +
    +Configuration Options + + +testMode: Defines the mode for acceptance tests. By default, the mode is MockMvc, +which is based on Spring’s MockMvc. It can also be changed to WebTestClient, JaxRsClient or to +Explicit for real HTTP calls. + + +basePackageForTests: Specifies the base package for all generated tests. If not set, +the value is picked from baseClassForTests’s package and from `packageWithBaseClasses. +If neither of these values are set, then the value is set to +org.springframework.cloud.contract.verifier.tests. + + +ruleClassForTests: Specifies a rule that should be added to the generated test +classes. + + +baseClassForTests: Creates a base class for all generated tests. By default, if you +use Spock classes, the class is spock.lang.Specification. + + +contractsDirectory: Specifies a directory containing contracts written with the +GroovyDSL. The default directory is /src/test/resources/contracts. + + +generatedTestSourcesDir: Specifies the test source directory where tests generated +from the Groovy DSL should be placed. By default its value is +$buildDir/generated-test-sources/contracts. + + +generatedTestResourcesDir: Specifies the test resource directory where resources used by the tests generated + + +testFramework: Specifies the target test framework to be used. Currently, Spock, JUnit 4 (TestFramework.JUNIT) and +JUnit 5 are supported with JUnit 4 being the default framework. + + +packageWithBaseClasses: Defines a package where all the base classes reside. This +setting takes precedence over baseClassForTests. The convention is such that, if you +have a contract under (for example) src/test/resources/contract/foo/bar/baz/ and set +the value of the packageWithBaseClasses property to com.example.base, then Spring +Cloud Contract Verifier assumes that there is a BarBazBase class under the +com.example.base package. In other words, the system takes the last two parts of the +package, if they exist, and forms a class with a Base suffix. + + +baseClassMappings: Specifies a list of base class mappings that provide +contractPackageRegex, which is checked against the package where the contract is +located, and baseClassFQN, which maps to the fully qualified name of the base class for +the matched contract. For example, if you have a contract under +src/test/resources/contract/foo/bar/baz/ and map the property +.* → com.example.base.BaseClass, then the test class generated from these contracts +extends com.example.base.BaseClass. This setting takes precedence over +packageWithBaseClasses and baseClassForTests. + + +contractsProperties: a map containing properties to be passed to Spring Cloud Contract +components. Those properties might be used by e.g. inbuilt or custom Stub Downloaders. + + +If you want to download your contract definitions from a Maven repository, you can use +the following options: + + +contractDependency: The contract dependency that contains all the packaged contracts. + + +contractsPath: The path to the concrete contracts in the JAR with packaged contracts. +Defaults to groupid/artifactid where gropuid is slash separated. + + +contractsMode: Picks the mode in which stubs will be found and registered + + +deleteStubsAfterTest: If set to false will not remove any downloaded +contracts from temporary directories + + +contractsRepositoryUrl: URL to a repo with the artifacts that have contracts. If it is not provided, +use the current Maven ones. + + +contractsRepositoryUsername: The user name to be used to connect to the repo with contracts. + + +contractsRepositoryPassword: The password to be used to connect to the repo with contracts. + + +contractsRepositoryProxyHost: The proxy host to be used to connect to the repo with contracts. + + +contractsRepositoryProxyPort: The proxy port to be used to connect to the repo with contracts. + + +We cache only non-snapshot, explicitly provided versions (for example ++ or 1.0.0.BUILD-SNAPSHOT won’t get cached). By default, this feature is turned on. +Below you can find a list of experimental features you can turn on via the plugin: + + +convertToYaml: converts all DSLs to the declarative, YAML format. This can be extremely useful when you’re using external libraries in your Groovy DSLs. By turning this feature on (by setting it to true) you will not need to add the library dependency on the consumer side. + + +assertJsonSize: You can check the size of JSON arrays in the generated tests. This feature is disabled by default. + + +
    +
    +Single Base Class for All Tests +When using Spring Cloud Contract Verifier in default MockMvc, you need to create a base +specification for all generated acceptance tests. In this class, you need to point to an +endpoint, which should be verified. +package org.mycompany.tests + +import org.mycompany.ExampleSpringController +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc +import spock.lang.Specification + +class MvcSpec extends Specification { + def setup() { + RestAssuredMockMvc.standaloneSetup(new ExampleSpringController()) + } +} +You can also setup the whole context if necessary. +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SomeConfig.class, properties="some=property") +public abstract class BaseTestClass { + + @Autowired + WebApplicationContext context; + + @Before + public void setup() { + RestAssuredMockMvc.webAppContextSetup(this.context); + } +} +If you use EXPLICIT mode, you can use a base class to initialize the whole tested app +similarly, as you might find in regular integration tests. +import io.restassured.RestAssured; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = SomeConfig.class, properties="some=property") +public abstract class BaseTestClass { + + @LocalServerPort + int port; + + @Before + public void setup() { + RestAssured.baseURI = "http://localhost:" + this.port; + } +} +If you use the JAXRSCLIENT mode, this base class should also contain a protected WebTarget webTarget field. Right +now, the only option to test the JAX-RS API is to start a web server. +
    +
    +Different base classes for contracts +If your base classes differ between contracts, you can tell the Spring Cloud Contract +plugin which class should get extended by the autogenerated tests. You have two options: + + +Follow a convention by providing the packageWithBaseClasses + + +provide explicit mapping via baseClassMappings + + +By Convention +The convention is such that if you have a contract under (for example) +src/test/resources/contract/foo/bar/baz/ and set the value of the +packageWithBaseClasses property to com.example.base, then Spring Cloud Contract +Verifier assumes that there is a BarBazBase class under the com.example.base package. +In other words, the system takes the last two parts of the package, if they exist, and +forms a class with a Base suffix. This rule takes precedence over baseClassForTests. +Here is an example of how it works in the contracts closure: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <configuration> + <packageWithBaseClasses>hello</packageWithBaseClasses> + </configuration> +</plugin> +By Mapping +You can manually map a regular expression of the contract’s package to fully qualified +name of the base class for the matched contract. You have to provide a list called +baseClassMappings that consists baseClassMapping objects that takes a +contractPackageRegex to baseClassFQN mapping. Consider the following example: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <configuration> + <baseClassForTests>com.example.FooBase</baseClassForTests> + <baseClassMappings> + <baseClassMapping> + <contractPackageRegex>.*com.*</contractPackageRegex> + <baseClassFQN>com.example.TestBase</baseClassFQN> + </baseClassMapping> + </baseClassMappings> + </configuration> +</plugin> +Assume that you have contracts under these two locations: +* src/test/resources/contract/com/ +* src/test/resources/contract/foo/ +By providing the baseClassForTests, we have a fallback in case mapping did not succeed. +(You can also provide the packageWithBaseClasses as a fallback.) That way, the tests +generated from src/test/resources/contract/com/ contracts extend the +com.example.ComBase, whereas the rest of the tests extend com.example.FooBase. +
    +
    +Invoking generated tests +The Spring Cloud Contract Maven Plugin generates verification code in a directory called +/generated-test-sources/contractVerifier and attaches this directory to testCompile +goal. +For Groovy Spock code, use the following: +<plugin> + <groupId>org.codehaus.gmavenplus</groupId> + <artifactId>gmavenplus-plugin</artifactId> + <version>1.5</version> + <executions> + <execution> + <goals> + <goal>testCompile</goal> + </goals> + </execution> + </executions> + <configuration> + <testSources> + <testSource> + <directory>${project.basedir}/src/test/groovy</directory> + <includes> + <include>**/*.groovy</include> + </includes> + </testSource> + <testSource> + <directory>${project.build.directory}/generated-test-sources/contractVerifier</directory> + <includes> + <include>**/*.groovy</include> + </includes> + </testSource> + </testSources> + </configuration> +</plugin> +To ensure that provider side is compliant with defined contracts, you need to invoke +mvn generateTest test. +
    +
    +Pushing stubs to SCM +If you’re using the SCM repository to keep the contracts and +stubs, you might want to automate the step of pushing stubs to +the repository. To do that, it’s enough to add the pushStubsToScm +goal. Example: +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <!-- Base class mappings etc. --> + + <!-- We want to pick contracts from a Git repository --> + <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl> + + <!-- We reuse the contract dependency section to set up the path + to the folder that contains the contract definitions. In our case the + path will be /groupId/artifactId/version/contracts --> + <contractDependency> + <groupId>${project.groupId}</groupId> + <artifactId>${project.artifactId}</artifactId> + <version>${project.version}</version> + </contractDependency> + + <!-- The contracts mode can't be classpath --> + <contractsMode>REMOTE</contractsMode> + </configuration> + <executions> + <execution> + <phase>package</phase> + <goals> + <!-- By default we will not push the stubs back to SCM, + you have to explicitly add it as a goal --> + <goal>pushStubsToScm</goal> + </goals> + </execution> + </executions> +</plugin> +Under you can find all possible +configuration options that you can pass either via +the <configuration><contractProperties> map, a system property +or an environment variable. +
    +
    +Maven Plugin and STS +If you see the following exception while using STS: + + + + + +STS Exception + + +When you click on the error marker you should see something like this: + plugin:1.1.0.M1:convert:default-convert:process-test-resources) org.apache.maven.plugin.PluginExecutionException: Execution default-convert of goal org.springframework.cloud:spring- + cloud-contract-maven-plugin:1.1.0.M1:convert failed. at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:145) at + org.eclipse.m2e.core.internal.embedder.MavenImpl.execute(MavenImpl.java:331) at org.eclipse.m2e.core.internal.embedder.MavenImpl$11.call(MavenImpl.java:1362) at +... + org.eclipse.core.internal.jobs.Worker.run(Worker.java:55) Caused by: java.lang.NullPointerException at + org.eclipse.m2e.core.internal.builder.plexusbuildapi.EclipseIncrementalBuildContext.hasDelta(EclipseIncrementalBuildContext.java:53) at + org.sonatype.plexus.build.incremental.ThreadBuildContext.hasDelta(ThreadBuildContext.java:59) at +In order to fix this issue, provide the following section in your pom.xml: +<build> + <pluginManagement> + <plugins> + <!--This plugin's configuration is used to store Eclipse m2e settings + only. It has no influence on the Maven build itself. --> + <plugin> + <groupId>org.eclipse.m2e</groupId> + <artifactId>lifecycle-mapping</artifactId> + <version>1.0.0</version> + <configuration> + <lifecycleMappingMetadata> + <pluginExecutions> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <versionRange>[1.0,)</versionRange> + <goals> + <goal>convert</goal> + </goals> + </pluginExecutionFilter> + <action> + <execute /> + </action> + </pluginExecution> + </pluginExecutions> + </lifecycleMappingMetadata> + </configuration> + </plugin> + </plugins> + </pluginManagement> +</build> +
    +
    +Maven Plugin with Spock Tests +You can select the Spock Framework for creating and executing the auto-generated contract +verification tests with both Maven and Gradle plugin. However, whereas with Gradle its really straightforward, +in Maven you will require some additional setup in order to make the tests compile and execute properly. +First of all, you will have to use a plugin, such as GMavenPlus plugin, +to add Groovy to your project. In GMavenPlus plugin, you will need to explicitly set test sources, including both the +path where your base test classes are defined and the path were the generated contract tests are added. +Please refer to the example below: +<plugin> + <groupId>org.codehaus.gmavenplus</groupId> + <artifactId>gmavenplus-plugin</artifactId> + <version>1.6.1</version> + <executions> + <execution> + <goals> + <goal>compileTests</goal> + <goal>addTestSources</goal> + </goals> + </execution> + </executions> + <configuration> + <testSources> + <testSource> + <directory>${project.basedir}/src/test/groovy</directory> + <includes> + <include>**/*.groovy</include> + </includes> + </testSource> + <testSource> + <directory> + ${project.basedir}/target/generated-test-sources/contracts/com/example/beer + </directory> + <includes> + <include>**/*.groovy</include> + <include>**/*.gvy</include> + </includes> + </testSource> + </testSources> + </configuration> + <dependencies> + <dependency> + <groupId>org.codehaus.groovy</groupId> + <artifactId>groovy-all</artifactId> + <version>2.4.15</version> + <scope>runtime</scope> + <type>pom</type> + </dependency> + </dependencies> +If you uphold to the Spock convention of ending the test class names with Spec, you will also need to adjust your Maven +Surefire plugin setup, like in the following example: + +
    +
    +
    +Stubs and Transitive Dependencies +The Maven and Gradle plugin that add the tasks that create the stubs jar for you. One +problem that arises is that, when reusing the stubs, you can mistakenly import all of +that stub’s dependencies. When building a Maven artifact, even though you have a couple +of different jars, all of them share one pom: +├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar +├── github-webhook-0.0.1.BUILD-20160903.075506-1-stubs.jar.sha1 +├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar +├── github-webhook-0.0.1.BUILD-20160903.075655-2-stubs.jar.sha1 +├── github-webhook-0.0.1.BUILD-SNAPSHOT.jar +├── github-webhook-0.0.1.BUILD-SNAPSHOT.pom +├── github-webhook-0.0.1.BUILD-SNAPSHOT-stubs.jar +├── ... +└── ... +There are three possibilities of working with those dependencies so as not to have any +issues with transitive dependencies: + + +Mark all application dependencies as optional + + +Create a separate artifactid for the stubs + + +Exclude dependencies on the consumer side + + +Mark all application dependencies as optional +If, in the github-webhook application, you mark all of your dependencies as optional, +when you include the github-webhook stubs in another application (or when that +dependency gets downloaded by Stub Runner) then, since all of the dependencies are +optional, they will not get downloaded. +Create a separate artifactid for the stubs +If you create a separate artifactid, then you can set it up in whatever way you wish. +For example, you might decide to have no dependencies at all. +Exclude dependencies on the consumer side +As a consumer, if you add the stub dependency to your classpath, you can explicitly +exclude the unwanted dependencies. +
    +
    +Scenarios +You can handle scenarios with Spring Cloud Contract Verifier. All you need to do is to +stick to the proper naming convention while creating your contracts. The convention +requires including an order number followed by an underscore. This will work regardles + of whether you’re working with YAML or Groovy. Example: +my_contracts_dir\ + scenario1\ + 1_login.groovy + 2_showCart.groovy + 3_logout.groovy +Such a tree causes Spring Cloud Contract Verifier to generate WireMock’s scenario with a +name of scenario1 and the three following steps: + + +login marked as Started pointing to…​ + + +showCart marked as Step1 pointing to…​ + + +logout marked as Step2 which will close the scenario. + + +More details about WireMock scenarios can be found at +https://wiremock.org/docs/stateful-behaviour/ +Spring Cloud Contract Verifier also generates tests with a guaranteed order of execution. +
    +
    +Docker Project +We’re publishing a springcloud/spring-cloud-contract Docker image +that contains a project that will generate tests and execute them in EXPLICIT mode +against a running application. + +The EXPLICIT mode means that the tests generated from contracts will send +real requests and not the mocked ones. + +
    +Short intro to Maven, JARs and Binary storage +Since the Docker image can be used by non JVM projects, it’s good to +explain the basic terms behind Spring Cloud Contract packaging defaults. +Part of the following definitions were taken from the Maven Glossary + + +Project: Maven thinks in terms of projects. Everything that you +will build are projects. Those projects follow a well defined +“Project Object Model”. Projects can depend on other projects, +in which case the latter are called “dependencies”. A project may +consistent of several subprojects, however these subprojects are still +treated equally as projects. + + +Artifact: An artifact is something that is either produced or used +by a project. Examples of artifacts produced by Maven for a project +include: JARs, source and binary distributions. Each artifact +is uniquely identified by a group id and an artifact ID which is +unique within a group. + + +JAR: JAR stands for Java ARchive. It’s a format based on +the ZIP file format. Spring Cloud Contract packages the contracts and generated +stubs in a JAR file. + + +GroupId: A group ID is a universally unique identifier for a project. +While this is often just the project name (eg. commons-collections), +it is helpful to use a fully-qualified package name to distinguish it +from other projects with a similar name (eg. org.apache.maven). +Typically, when published to the Artifact Manager, the GroupId will get +slash separated and form part of the URL. E.g. for group id com.example +and artifact id application would be /com/example/application/. + + +Classifier: The Maven dependency notation looks as follows: +groupId:artifactId:version:classifier. The classifier is additional suffix +passed to the dependency. E.g. stubs, sources. The same dependency +e.g. com.example:application can produce multiple artifacts that +differ from each other with the classifier. + + +Artifact manager: When you generate binaries / sources / packages, you would +like them to be available for others to download / reference or reuse. In case +of the JVM world those artifacts would be JARs, for Ruby these are gems +and for Docker those would be Docker images. You can store those artifacts +in a manager. Examples of such managers can be Artifactory +or Nexus. + + +
    +
    +How it works +The image searches for contracts under the /contracts folder. +The output from running the tests will be available under +/spring-cloud-contract/build folder (it’s useful for debugging +purposes). +It’s enough for you to mount your contracts, pass the environment variables + and the image will: + + +generate the contract tests + + +execute the tests against the provided URL + + +generate the WireMock stubs + + +(optional - turned on by default) publish the stubs to a Artifact Manager + + +
    +Environment Variables +The Docker image requires some environment variables to point to +your running application, to the Artifact manager instance etc. + + +PROJECT_GROUP - your project’s group id. Defaults to com.example + + +PROJECT_VERSION - your project’s version. Defaults to 0.0.1-SNAPSHOT + + +PROJECT_NAME - artifact id. Defaults to example + + +REPO_WITH_BINARIES_URL - URL of your Artifact Manager. Defaults to http://localhost:8081/artifactory/libs-release-local +which is the default URL of Artifactory running locally + + +REPO_WITH_BINARIES_USERNAME - (optional) username when the Artifact Manager is secured + + +REPO_WITH_BINARIES_PASSWORD - (optional) password when the Artifact Manager is secured + + +PUBLISH_ARTIFACTS - if set to true then will publish artifact to binary storage. Defaults to true. + + +These environment variables are used when contracts lay in an external repository. To enable +this feature you must set the EXTERNAL_CONTRACTS_ARTIFACT_ID environment variable. + + +EXTERNAL_CONTRACTS_GROUP_ID - group id of the project with contracts. Defaults to com.example + + +EXTERNAL_CONTRACTS_ARTIFACT_ID- artifact id of the project with contracts. + + +EXTERNAL_CONTRACTS_CLASSIFIER- classifier of the project with contracts. Empty by default + + +EXTERNAL_CONTRACTS_VERSION - version of the project with contracts. Defaults to +, equivalent to picking the latest + + +EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL - URL of your Artifact Manager. Defaults to value of REPO_WITH_BINARIES_URL env var. +If that’s not set, defaults to http://localhost:8081/artifactory/libs-release-local +which is the default URL of Artifactory running locally + + +EXTERNAL_CONTRACTS_PATH - path to contracts for the given project, inside the project with contracts. +Defaults to slash separated EXTERNAL_CONTRACTS_GROUP_ID concatenated with / and EXTERNAL_CONTRACTS_ARTIFACT_ID. E.g. +for group id foo.bar and artifact id baz, would result in foo/bar/baz contracts path. + + +EXTERNAL_CONTRACTS_WORK_OFFLINE - if set to true then will retrieve artifact with contracts +from the container’s .m2. Mount your local .m2 as a volume available at the container’s /root/.m2 path. +You must not set both EXTERNAL_CONTRACTS_WORK_OFFLINE and EXTERNAL_CONTRACTS_REPO_WITH_BINARIES_URL. + + +These environment variables are used when tests are executed: + + +APPLICATION_BASE_URL - url against which tests should be executed. +Remember that it has to be accessible from the Docker container (e.g. localhost +will not work) + + +APPLICATION_USERNAME - (optional) username for basic authentication to your application + + +APPLICATION_PASSWORD - (optional) password for basic authentication to your application + + +
    +
    +
    +Example of usage +Let’s take a look at a simple MVC application +$ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs +$ cd bookstore +The contracts are available under /contracts folder. +
    +
    +Server side (nodejs) +Since we want to run tests, we could just execute: +$ npm test +however, for learning purposes, let’s split it into pieces: +# Stop docker infra (nodejs, artifactory) +$ ./stop_infra.sh +# Start docker infra (nodejs, artifactory) +$ ./setup_infra.sh + +# Kill & Run app +$ pkill -f "node app" +$ nohup node app & + +# Prepare environment variables +$ SC_CONTRACT_DOCKER_VERSION="..." +$ APP_IP="192.168.0.100" +$ APP_PORT="3000" +$ ARTIFACTORY_PORT="8081" +$ APPLICATION_BASE_URL="http://${APP_IP}:${APP_PORT}" +$ ARTIFACTORY_URL="http://${APP_IP}:${ARTIFACTORY_PORT}/artifactory/libs-release-local" +$ CURRENT_DIR="$( pwd )" +$ CURRENT_FOLDER_NAME=${PWD##*/} +$ PROJECT_VERSION="0.0.1.RELEASE" + +# Execute contract tests +$ docker run --rm -e "APPLICATION_BASE_URL=${APPLICATION_BASE_URL}" -e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=${CURRENT_FOLDER_NAME}" -e "REPO_WITH_BINARIES_URL=${ARTIFACTORY_URL}" -e "PROJECT_VERSION=${PROJECT_VERSION}" -v "${CURRENT_DIR}/contracts/:/contracts:ro" -v "${CURRENT_DIR}/node_modules/spring-cloud-contract/output:/spring-cloud-contract-output/" springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}" + +# Kill app +$ pkill -f "node app" +What will happen is that via bash scripts: + + +infrastructure will be set up (MongoDb, Artifactory). +In real life scenario you would just run the NodeJS application +with mocked database. In this example we want to show how we can +benefit from Spring Cloud Contract in no time. + + +due to those constraints the contracts also represent the +stateful situation + + +first request is a POST that causes data to get inserted to the database + + +second request is a GET that returns a list of data with 1 previously inserted element + + + + +the NodeJS application will be started (on port 3000) + + +contract tests will be generated via Docker and tests +will be executed against the running application + + +the contracts will be taken from /contracts folder. + + +the output of the test execution is available under +node_modules/spring-cloud-contract/output. + + + + +the stubs will be uploaded to Artifactory. You can check them out +under http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/ . +The stubs will be here http://localhost:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/bookstore-0.0.1.RELEASE-stubs.jar. + + +To see how the client side looks like check out the section. +
    +
    +
    + +Spring Cloud Contract Verifier Messaging +Spring Cloud Contract Verifier lets you verify applications that use messaging as a +means of communication. All of the integrations shown in this document work with Spring, +but you can also create one of your own and use that. +
    +Integrations +You can use one of the following four integration configurations: + + +Apache Camel + + +Spring Integration + + +Spring Cloud Stream + + +Spring AMQP + + +Since we use Spring Boot, if you have added one of these libraries to the classpath, all +the messaging configuration is automatically set up. + +Remember to put @AutoConfigureMessageVerifier on the base class of your +generated tests. Otherwise, messaging part of Spring Cloud Contract Verifier does not +work. + + +If you want to use Spring Cloud Stream, remember to add a dependency on +org.springframework.cloud:spring-cloud-stream-test-support, as shown here: + + +Maven + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-test-support</artifactId> + <scope>test</scope> +</dependency> + + + +Gradle + +testCompile "org.springframework.cloud:spring-cloud-stream-test-support" + + +
    +
    +Manual Integration Testing +The main interface used by the tests is +org.springframework.cloud.contract.verifier.messaging.MessageVerifier. +It defines how to send and receive messages. You can create your own implementation to +achieve the same goal. +In a test, you can inject a ContractVerifierMessageExchange to send and receive +messages that follow the contract. Then add @AutoConfigureMessageVerifier to your test. +Here’s an example: +@RunWith(SpringTestRunner.class) +@SpringBootTest +@AutoConfigureMessageVerifier +public static class MessagingContractTests { + + @Autowired + private MessageVerifier verifier; + ... +} + +If your tests require stubs as well, then @AutoConfigureStubRunner includes the +messaging configuration, so you only need the one annotation. + +
    +
    +Publisher-Side Test Generation +Having the input or outputMessage sections in your DSL results in creation of tests +on the publisher’s side. By default, JUnit 4 tests are created. However, there is also a +possibility to create JUnit 5 or Spock tests. +There are 3 main scenarios that we should take into consideration: + + +Scenario 1: There is no input message that produces an output message. The output +message is triggered by a component inside the application (for example, scheduler). + + +Scenario 2: The input message triggers an output message. + + +Scenario 3: The input message is consumed and there is no output message. + + + +The destination passed to messageFrom or sentTo can have different +meanings for different messaging implementations. For Stream and Integration it is +first resolved as a destination of a channel. Then, if there is no such destination +it is resolved as a channel name. For Camel, that’s a certain component (for example, +jms). + +
    +Scenario 1: No Input Message +For the given contract: + +Groovy DSL + + def contractDsl = Contract.make { + label 'some_label' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('activemq:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + messagingContentType(applicationJson()) + } + } + } + + + +YAML + +label: some_label +input: + triggeredBy: bookReturnedTriggered +outputMessage: + sentTo: activemq:output + body: + bookName: foo + headers: + BOOK-NAME: foo + contentType: application/json + + +The following JUnit test is created: + ''' + // when: + bookReturnedTriggered(); + + // then: + ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output"); + assertThat(response).isNotNull(); + assertThat(response.getHeader("BOOK-NAME")).isNotNull(); + assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo"); + assertThat(response.getHeader("contentType")).isNotNull(); + assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json"); + // and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); + assertThatJson(parsedJson).field("bookName").isEqualTo("foo"); +''' +And the following Spock test would be created: + ''' + when: + bookReturnedTriggered() + + then: + ContractVerifierMessage response = contractVerifierMessaging.receive('activemq:output') + assert response != null + response.getHeader('BOOK-NAME')?.toString() == 'foo' + response.getHeader('contentType')?.toString() == 'application/json' + and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + assertThatJson(parsedJson).field("bookName").isEqualTo("foo") + +''' +
    +
    +Scenario 2: Output Triggered by Input +For the given contract: + +Groovy DSL + + def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } + } + + + +YAML + +label: some_label +input: + messageFrom: jms:input + messageBody: + bookName: 'foo' + messageHeaders: + sample: header +outputMessage: + sentTo: jms:output + body: + bookName: foo + headers: + BOOK-NAME: foo + + +The following JUnit test is created: + ''' +// given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + "{\\"bookName\\":\\"foo\\"}" +, headers() + .header("sample", "header")); + +// when: + contractVerifierMessaging.send(inputMessage, "jms:input"); + +// then: + ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output"); + assertThat(response).isNotNull(); + assertThat(response.getHeader("BOOK-NAME")).isNotNull(); + assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo"); +// and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload())); + assertThatJson(parsedJson).field("bookName").isEqualTo("foo"); +''' +And the following Spock test would be created: + """\ +given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + '''{"bookName":"foo"}''', + ['sample': 'header'] + ) + +when: + contractVerifierMessaging.send(inputMessage, 'jms:input') + +then: + ContractVerifierMessage response = contractVerifierMessaging.receive('jms:output') + assert response !- null + response.getHeader('BOOK-NAME')?.toString() == 'foo' +and: + DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload)) + assertThatJson(parsedJson).field("bookName").isEqualTo("foo") +""" +
    +
    +Scenario 3: No Output Message +For the given contract: + +Groovy DSL + + def contractDsl = Contract.make { + label 'some_label' + input { + messageFrom('jms:delete') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + assertThat('bookWasDeleted()') + } + } + + + +YAML + +label: some_label +input: + messageFrom: jms:delete + messageBody: + bookName: 'foo' + messageHeaders: + sample: header + assertThat: bookWasDeleted() + + +The following JUnit test is created: + ''' +// given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + "{\\"bookName\\":\\"foo\\"}" +, headers() + .header("sample", "header")); + +// when: + contractVerifierMessaging.send(inputMessage, "jms:delete"); + +// then: + bookWasDeleted(); +''' +And the following Spock test would be created: + ''' +given: + ContractVerifierMessage inputMessage = contractVerifierMessaging.create( + \'\'\'{"bookName":"foo"}\'\'\', + ['sample': 'header'] + ) + +when: + contractVerifierMessaging.send(inputMessage, 'jms:delete') + +then: + noExceptionThrown() + bookWasDeleted() +''' +
    +
    +
    +Consumer Stub Generation +Unlike the HTTP part, in messaging, we need to publish the Groovy DSL inside the JAR with +a stub. Then it is parsed on the consumer side and proper stubbed routes are created. +For more information, see section. + +Maven + +<dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-stream-rabbit</artifactId> + </dependency> + + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-test-support</artifactId> + <scope>test</scope> + </dependency> +</dependencies> + +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-dependencies</artifactId> + <version>Greenwich.BUILD-SNAPSHOT</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> + + + +Gradle + +ext { + contractsDir = file("mappings") + stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/") +} + +// Automatically added by plugin: +// copyContracts - copies contracts to the output folder from which JAR will be created +// verifierStubsJar - JAR with a provided stub suffix +// the presented publication is also added by the plugin but you can modify it as you wish + +publishing { + publications { + stubs(MavenPublication) { + artifactId "${project.name}-stubs" + artifact verifierStubsJar + } + } +} + + +
    +
    + +Spring Cloud Contract Stub Runner +One of the issues that you might encounter while using Spring Cloud Contract Verifier is +passing the generated WireMock JSON stubs from the server side to the client side (or to +various clients). The same takes place in terms of client-side generation for messaging. +Copying the JSON files and setting the client side for messaging manually is out of the +question. That is why we introduced Spring Cloud Contract Stub Runner. It can +automatically download and run the stubs for you. +
    +Snapshot versions +Add the additional snapshot repository to your build.gradle file to use snapshot +versions, which are automatically uploaded after every successful build: + +Maven + +<repositories> + <repository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </repository> + <repository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> + <repository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </repository> +</repositories> +<pluginRepositories> + <pluginRepository> + <id>spring-snapshots</id> + <name>Spring Snapshots</name> + <url>https://repo.spring.io/snapshot</url> + <snapshots> + <enabled>true</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-milestones</id> + <name>Spring Milestones</name> + <url>https://repo.spring.io/milestone</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> + <pluginRepository> + <id>spring-releases</id> + <name>Spring Releases</name> + <url>https://repo.spring.io/release</url> + <snapshots> + <enabled>false</enabled> + </snapshots> + </pluginRepository> +</pluginRepositories> + + + +Gradle + +/* + We need to use the [buildscript {}] section when we have to modify + the classpath for the plugins. If that's not the case this section + can be skipped. + + If you don't need to modify the classpath (e.g. add a Pact dependency), + then you can just set the [pluginManagement {}] section in [settings.gradle] file. + + // settings.gradle + pluginManagement { + repositories { + // for snapshots + maven {url "https://repo.spring.io/snapshot"} + // for milestones + maven {url "https://repo.spring.io/milestone"} + // for GA versions + gradlePluginPortal() + } + } + + */ +buildscript { + repositories { + mavenCentral() + mavenLocal() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + maven { url "https://repo.spring.io/release" } + } + + +
    +
    +Publishing Stubs as JARs +The easiest approach would be to centralize the way stubs are kept. For example, you can +keep them as jars in a Maven repository. + +For both Maven and Gradle, the setup comes ready to work. However, you can customize +it if you want to. + + +Maven + +<!-- First disable the default jar setup in the properties section --> +<!-- we don't want the verifier to do a jar for us --> +<spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip> + +<!-- Next add the assembly plugin to your build --> +<!-- we want the assembly plugin to generate the JAR --> +<plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <id>stub</id> + <phase>prepare-package</phase> + <goals> + <goal>single</goal> + </goals> + <inherited>false</inherited> + <configuration> + <attach>true</attach> + <descriptors> + $../../../../src/assembly/stub.xml + </descriptors> + </configuration> + </execution> + </executions> +</plugin> + +<!-- Finally setup your assembly. Below you can find the contents of src/main/assembly/stub.xml --> +<assembly + xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd"> + <id>stubs</id> + <formats> + <format>jar</format> + </formats> + <includeBaseDirectory>false</includeBaseDirectory> + <fileSets> + <fileSet> + <directory>src/main/java</directory> + <outputDirectory>/</outputDirectory> + <includes> + <include>**com/example/model/*.*</include> + </includes> + </fileSet> + <fileSet> + <directory>${project.build.directory}/classes</directory> + <outputDirectory>/</outputDirectory> + <includes> + <include>**com/example/model/*.*</include> + </includes> + </fileSet> + <fileSet> + <directory>${project.build.directory}/snippets/stubs</directory> + <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings</outputDirectory> + <includes> + <include>**/*</include> + </includes> + </fileSet> + <fileSet> + <directory>$../../../../src/test/resources/contracts</directory> + <outputDirectory>META-INF/${project.groupId}/${project.artifactId}/${project.version}/contracts</outputDirectory> + <includes> + <include>**/*.groovy</include> + </includes> + </fileSet> + </fileSets> +</assembly> + + + +Gradle + +ext { + contractsDir = file("mappings") + stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/") +} + +// Automatically added by plugin: +// copyContracts - copies contracts to the output folder from which JAR will be created +// verifierStubsJar - JAR with a provided stub suffix +// the presented publication is also added by the plugin but you can modify it as you wish + +publishing { + publications { + stubs(MavenPublication) { + artifactId "${project.name}-stubs" + artifact verifierStubsJar + } + } +} + + +
    +
    +Stub Runner Core +Runs stubs for service collaborators. Treating stubs as contracts of services allows to use stub-runner as an implementation of +Consumer Driven Contracts. +Stub Runner allows you to automatically download the stubs of the provided dependencies (or pick those from the classpath), start WireMock servers for them and feed them with proper stub definitions. +For messaging, special stub routes are defined. +
    +Retrieving stubs +You can pick the following options of acquiring stubs + + +Aether based solution that downloads JARs with stubs from Artifactory / Nexus + + +Classpath scanning solution that searches classpath via pattern to retrieve stubs + + +Write your own implementation of the org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder for full customization + + +The latter example is described in the Custom Stub Runner section. +
    +Stub downloading +You can control the stub downloading via the stubsMode switch. It picks value from the +StubRunnerProperties.StubsMode enum. You can use the following options + + +StubRunnerProperties.StubsMode.CLASSPATH (default value) - will pick stubs from the classpath + + +StubRunnerProperties.StubsMode.LOCAL - will pick stubs from a local storage (e.g. .m2) + + +StubRunnerProperties.StubsMode.REMOTE - will pick stubs from a remote location + + +Example: +@AutoConfigureStubRunner(repositoryRoot="https://foo.bar", ids = "com.example:beer-api-producer:+:stubs:8095", stubsMode = StubRunnerProperties.StubsMode.LOCAL) +
    +
    +Classpath scanning +If you set the stubsMode property to StubRunnerProperties.StubsMode.CLASSPATH +(or set nothing since CLASSPATH is the default value) then classpath will get scanned. +Let’s look at the following example: +@AutoConfigureStubRunner(ids = { + "com.example:beer-api-producer:+:stubs:8095", + "com.example.foo:bar:1.0.0:superstubs:8096" +}) +If you’ve added the dependencies to your classpath + +Maven + +<dependency> + <groupId>com.example</groupId> + <artifactId>beer-api-producer-restdocs</artifactId> + <classifier>stubs</classifier> + <version>0.0.1-SNAPSHOT</version> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> +</dependency> +<dependency> + <groupId>com.example.foo</groupId> + <artifactId>bar</artifactId> + <classifier>superstubs</classifier> + <version>1.0.0</version> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> +</dependency> + + + +Gradle + +testCompile("com.example:beer-api-producer-restdocs:0.0.1-SNAPSHOT:stubs") { + transitive = false +} +testCompile("com.example.foo:bar:1.0.0:superstubs") { + transitive = false +} + + +Then the following locations on your classpath will get scanned. For com.example:beer-api-producer-restdocs + + +/META-INF/com.example/beer-api-producer-restdocs/*/.* + + +/contracts/com.example/beer-api-producer-restdocs/*/.* + + +/mappings/com.example/beer-api-producer-restdocs/*/.* + + +and com.example.foo:bar + + +/META-INF/com.example.foo/bar/*/.* + + +/contracts/com.example.foo/bar/*/.* + + +/mappings/com.example.foo/bar/*/.* + + + +As you can see you have to explicitly provide the group and artifact ids when packaging the +producer stubs. + +The producer would setup the contracts like this: +└── src + └── test + └── resources + └── contracts +    └── com.example +       └── beer-api-producer-restdocs +       └── nested +       └── contract3.groovy +To achieve proper stub packaging. +Or using the Maven assembly plugin or +Gradle Jar task you have to create the following +structure in your stubs jar. +└── META-INF + └── com.example + └── beer-api-producer-restdocs + └── 2.0.0 + ├── contracts + │   └── nested +    │ └── contract2.groovy +    └── mappings +    └── mapping.json +By maintaining this structure classpath gets scanned and you can profit from the messaging / +HTTP stubs without the need to download artifacts. +
    +
    +Configuring HTTP Server Stubs +Stub Runner has a notion of a HttpServerStub that abstracts the underlaying +concrete implementation of the HTTP server (e.g. WireMock is one of the implementations). +Sometimes, you need to perform some additional tuning of the stub servers, +that is concrete for the given implementation. To do that, Stub Runner gives you +the httpServerStubConfigurer property that is available in the annotation, +JUnit rule, and is accessible via system properties, where you can provide +your implementation of the org.springframework.cloud.contract.stubrunner.HttpServerStubConfigurer interface. The implementations can alter +the configuration files for the given HTTP server stub. +Spring Cloud Contract Stub Runner comes with an implementation that you +can extend, for WireMock - org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStubConfigurer. In the configure method +you can provide your own, custom configuration for the given stub. The use +case might be starting WireMock for the given artifact id, on an HTTPs port. Example: + +WireMockHttpServerStubConfigurer implementation + +@CompileStatic +static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer { + + private static final Log log = LogFactory.getLog(HttpsForFraudDetection) + + @Override + WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) { + if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") { + int httpsPort = SocketUtils.findAvailableTcpPort() + log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server") + return httpStubConfiguration + .httpsPort(httpsPort) + } + return httpStubConfiguration + } +} + + +You can then reuse it via the annotation +@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/", + httpServerStubConfigurer = HttpsForFraudDetection) +Whenever an https port is found, it will take precedence over the http one. +
    +
    +
    +Running stubs +
    +Running using main app +You can set the following options to the main class: +-c, --classifier Suffix for the jar containing stubs (e. + g. 'stubs' if the stub jar would + have a 'stubs' classifier for stubs: + foobar-stubs ). Defaults to 'stubs' + (default: stubs) +--maxPort, --maxp <Integer> Maximum port value to be assigned to + the WireMock instance. Defaults to + 15000 (default: 15000) +--minPort, --minp <Integer> Minimum port value to be assigned to + the WireMock instance. Defaults to + 10000 (default: 10000) +-p, --password Password to user when connecting to + repository +--phost, --proxyHost Proxy host to use for repository + requests +--pport, --proxyPort [Integer] Proxy port to use for repository + requests +-r, --root Location of a Jar containing server + where you keep your stubs (e.g. http: + //nexus. + net/content/repositories/repository) +-s, --stubs Comma separated list of Ivy + representation of jars with stubs. + Eg. groupid:artifactid1,groupid2: + artifactid2:classifier +--sm, --stubsMode Stubs mode to be used. Acceptable values + [CLASSPATH, LOCAL, REMOTE] +-u, --username Username to user when connecting to + repository +
    +
    +HTTP Stubs +Stubs are defined in JSON documents, whose syntax is defined in WireMock documentation +Example: +{ + "request": { + "method": "GET", + "url": "/ping" + }, + "response": { + "status": 200, + "body": "pong", + "headers": { + "Content-Type": "text/plain" + } + } +} +
    +
    +Viewing registered mappings +Every stubbed collaborator exposes list of defined mappings under __/admin/ endpoint. +You can also use the mappingsOutputFolder property to dump the mappings to files. + For annotation based approach it would look like this +@AutoConfigureStubRunner(ids="a.b.c:loanIssuance,a.b.c:fraudDetectionServer", +mappingsOutputFolder = "target/outputmappings/") +and for the JUnit approach like this: +@ClassRule @Shared StubRunnerRule rule = new StubRunnerRule() + .repoRoot("http://some_url") + .downloadStub("a.b.c", "loanIssuance") + .downloadStub("a.b.c:fraudDetectionServer") + .withMappingsOutputFolder("target/outputmappings") +Then if you check out the folder target/outputmappings you would see the following structure +. +├── fraudDetectionServer_13705 +└── loanIssuance_12255 +That means that there were two stubs registered. fraudDetectionServer was registered at port 13705 +and loanIssuance at port 12255. If we take a look at one of the files we would see (for WireMock) +mappings available for the given server: +[{ + "id" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7", + "request" : { + "url" : "/name", + "method" : "GET" + }, + "response" : { + "status" : 200, + "body" : "fraudDetectionServer" + }, + "uuid" : "f9152eb9-bf77-4c38-8289-90be7d10d0d7" +}, +... +] +
    +
    +Messaging Stubs +Depending on the provided Stub Runner dependency and the DSL the messaging routes are automatically set up. +
    +
    +
    +
    +Stub Runner JUnit Rule and Stub Runner JUnit5 Extension +Stub Runner comes with a JUnit rule thanks to which you can very easily download and run stubs for given group and artifact id: +@ClassRule +public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot()) + .stubsMode(StubRunnerProperties.StubsMode.REMOTE) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", + "loanIssuance") + .downloadStub( + "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer"); + +@BeforeClass +@AfterClass +public static void setupProps() { + System.clearProperty("stubrunner.repository.root"); + System.clearProperty("stubrunner.classifier"); +} +There’s also a StubRunnerExtension available for JUnit 5. StubRunnerRule and StubRunnerExtension work in a very +similar fashion. After the rule/ extension is executed, Stub Runner connects to your Maven repository and for the given list of dependencies tries to: + + +download them + + +cache them locally + + +unzip them to a temporary folder + + +start a WireMock server for each Maven dependency on a random port from the provided range of ports / provided port + + +feed the WireMock server with all JSON files that are valid WireMock definitions + + +can also send messages (remember to pass an implementation of MessageVerifier interface) + + +Stub Runner uses Eclipse Aether mechanism to download the Maven dependencies. +Check their docs for more information. +Since the StubRunnerRule and StubRunnerExtension implement the StubFinder they allow you to find the started stubs: +package org.springframework.cloud.contract.stubrunner; + +import java.net.URL; +import java.util.Collection; +import java.util.Map; + +import org.springframework.cloud.contract.spec.Contract; + +/** + * Contract for finding registered stubs. + * + * @author Marcin Grzejszczak + */ +public interface StubFinder extends StubTrigger { + + /** + * For the given groupId and artifactId tries to find the matching URL of the running + * stub. + * @param groupId - might be null. In that case a search only via artifactId takes + * place + * @param artifactId - artifact id of the stub + * @return URL of a running stub or throws exception if not found + * @throws StubNotFoundException in case of not finding a stub + */ + URL findStubUrl(String groupId, String artifactId) throws StubNotFoundException; + + /** + * For the given Ivy notation {@code [groupId]:artifactId:[version]:[classifier]} + * tries to find the matching URL of the running stub. You can also pass only + * {@code artifactId}. + * @param ivyNotation - Ivy representation of the Maven artifact + * @return URL of a running stub or throws exception if not found + * @throws StubNotFoundException in case of not finding a stub + */ + URL findStubUrl(String ivyNotation) throws StubNotFoundException; + + /** + * @return all running stubs + */ + RunningStubs findAllRunningStubs(); + + /** + * @return the list of Contracts + */ + Map<StubConfiguration, Collection<Contract>> getContracts(); + +} +Example of usage in Spock tests: +@ClassRule +@Shared +StubRunnerRule rule = new StubRunnerRule() + .stubsMode(StubRunnerProperties.StubsMode.REMOTE) + .repoRoot(StubRunnerRuleSpec.getResource("/m2repo/repository").toURI().toString()) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", "loanIssuance") + .downloadStub("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer") + .withMappingsOutputFolder("target/outputmappingsforrule") + + +def 'should start WireMock servers'() { + expect: 'WireMocks are running' + rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null + rule.findStubUrl('loanIssuance') != null + rule.findStubUrl('loanIssuance') == rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') + rule.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null + and: + rule.findAllRunningStubs().isPresent('loanIssuance') + rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer') + rule.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') + and: 'Stubs were registered' + "${rule.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance' + "${rule.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer' +} + +def 'should output mappings to output folder'() { + when: + def url = rule.findStubUrl('fraudDetectionServer') + then: + new File("target/outputmappingsforrule", "fraudDetectionServer_${url.port}").exists() +} +Example of usage in JUnit tests: + @Test + public void should_start_wiremock_servers() throws Exception { + // expect: 'WireMocks are running' + then(rule.findStubUrl("org.springframework.cloud.contract.verifier.stubs", + "loanIssuance")).isNotNull(); + then(rule.findStubUrl("loanIssuance")).isNotNull(); + then(rule.findStubUrl("loanIssuance")).isEqualTo(rule.findStubUrl( + "org.springframework.cloud.contract.verifier.stubs", "loanIssuance")); + then(rule.findStubUrl( + "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")) + .isNotNull(); + // and: + then(rule.findAllRunningStubs().isPresent("loanIssuance")).isTrue(); + then(rule.findAllRunningStubs().isPresent( + "org.springframework.cloud.contract.verifier.stubs", + "fraudDetectionServer")).isTrue(); + then(rule.findAllRunningStubs().isPresent( + "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")) + .isTrue(); + // and: 'Stubs were registered' + then(httpGet(rule.findStubUrl("loanIssuance").toString() + "/name")) + .isEqualTo("loanIssuance"); + then(httpGet(rule.findStubUrl("fraudDetectionServer").toString() + "/name")) + .isEqualTo("fraudDetectionServer"); + } + + private String httpGet(String url) throws Exception { + try (InputStream stream = URI.create(url).toURL().openStream()) { + return StreamUtils.copyToString(stream, Charset.forName("UTF-8")); + } + } + +} +JUnit 5 Extension example: +// Visible for Junit +@RegisterExtension +static StubRunnerExtension stubRunnerExtension = new StubRunnerExtension() + .repoRoot(repoRoot()).stubsMode(StubRunnerProperties.StubsMode.REMOTE) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", + "loanIssuance") + .downloadStub( + "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer") + .withMappingsOutputFolder("target/outputmappingsforrule"); + +@BeforeAll +@AfterAll +static void setupProps() { + System.clearProperty("stubrunner.repository.root"); + System.clearProperty("stubrunner.classifier"); +} + +private static String repoRoot() { + try { + return StubRunnerRuleJUnitTest.class.getResource("/m2repo/repository/") + .toURI().toString(); + } + catch (Exception e) { + return ""; + } +} +Check the Common properties for JUnit and Spring for more information on how to apply global configuration of Stub Runner. + +To use the JUnit rule or JUnit 5 extension together with messaging, you have to provide an implementation of the +MessageVerifier interface to the rule builder (e.g. rule.messageVerifier(new MyMessageVerifier())). +If you don’t do this, then whenever you try to send a message an exception will be thrown. + +
    +Maven settings +The stub downloader honors Maven settings for a different local repository folder. +Authentication details for repositories and profiles are currently not taken into account, so you need to specify it using the properties mentioned above. +
    +
    +Providing fixed ports +You can also run your stubs on fixed ports. You can do it in two different ways. One is to pass it in the properties, and the other via fluent API of +JUnit rule. +
    +
    +Fluent API +When using the StubRunnerRule or StubRunnerExtension you can add a stub to download and then pass the port for the last downloaded stub. +@ClassRule +public static StubRunnerRule rule = new StubRunnerRule().repoRoot(repoRoot()) + .stubsMode(StubRunnerProperties.StubsMode.REMOTE) + .downloadStub("org.springframework.cloud.contract.verifier.stubs", + "loanIssuance") + .withPort(12345).downloadStub( + "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer:12346"); + +@BeforeClass +@AfterClass +public static void setupProps() { + System.clearProperty("stubrunner.repository.root"); + System.clearProperty("stubrunner.classifier"); +} +You can see that for this example the following test is valid: +then(rule.findStubUrl("loanIssuance")) + .isEqualTo(URI.create("http://localhost:12345").toURL()); +then(rule.findStubUrl("fraudDetectionServer")) + .isEqualTo(URI.create("http://localhost:12346").toURL()); +
    +
    +Stub Runner with Spring +Sets up Spring configuration of the Stub Runner project. +By providing a list of stubs inside your configuration file the Stub Runner automatically downloads +and registers in WireMock the selected stubs. +If you want to find the URL of your stubbed dependency you can autowire the StubFinder interface and use +its methods as presented below: +@ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +@SpringBootTest(properties = [" stubrunner.cloud.enabled=false", + 'foo=${stubrunner.runningstubs.fraudDetectionServer.port}', + 'fooWithGroup=${stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port}']) +@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/", + httpServerStubConfigurer = HttpsForFraudDetection) +@ActiveProfiles("test") +class StubRunnerConfigurationSpec extends Specification { + + @Autowired + StubFinder stubFinder + @Autowired + Environment environment + @StubRunnerPort("fraudDetectionServer") + int fraudDetectionServerPort + @StubRunnerPort("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer") + int fraudDetectionServerPortWithGroupId + @Value('${foo}') + Integer foo + + @BeforeClass + @AfterClass + void setupProps() { + System.clearProperty("stubrunner.repository.root") + System.clearProperty("stubrunner.classifier") + WireMockHttpServerStubAccessor.clear() + } + + def 'should mark all ports as random'() { + expect: + WireMockHttpServerStubAccessor.everyPortRandom() + } + + def 'should start WireMock servers'() { + expect: 'WireMocks are running' + stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null + stubFinder.findStubUrl('loanIssuance') != null + stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') + stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance') + stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs') + stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null + and: + stubFinder.findAllRunningStubs().isPresent('loanIssuance') + stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer') + stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') + and: 'Stubs were registered' + "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance' + "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer' + and: 'Fraud Detection is an HTTPS endpoint' + stubFinder.findStubUrl('fraudDetectionServer').toString().startsWith("https") + } + + def 'should throw an exception when stub is not found'() { + when: + stubFinder.findStubUrl('nonExistingService') + then: + thrown(StubNotFoundException) + when: + stubFinder.findStubUrl('nonExistingGroupId', 'nonExistingArtifactId') + then: + thrown(StubNotFoundException) + } + + def 'should register started servers as environment variables'() { + expect: + environment.getProperty("stubrunner.runningstubs.loanIssuance.port") != null + stubFinder.findAllRunningStubs().getPort("loanIssuance") == (environment.getProperty("stubrunner.runningstubs.loanIssuance.port") as Integer) + and: + environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null + stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") as Integer) + and: + environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null + stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port") as Integer) + } + + def 'should be able to interpolate a running stub in the passed test property'() { + given: + int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") + expect: + fraudPort > 0 + environment.getProperty("foo", Integer) == fraudPort + environment.getProperty("fooWithGroup", Integer) == fraudPort + foo == fraudPort + } + + @Issue("#573") + def 'should be able to retrieve the port of a running stub via an annotation'() { + given: + int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") + expect: + fraudPort > 0 + fraudDetectionServerPort == fraudPort + fraudDetectionServerPortWithGroupId == fraudPort + } + + def 'should dump all mappings to a file'() { + when: + def url = stubFinder.findStubUrl("fraudDetectionServer") + then: + new File("target/outputmappings/", "fraudDetectionServer_${url.port}").exists() + } + + @Configuration + @EnableAutoConfiguration + static class Config {} + + @CompileStatic + static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer { + + private static final Log log = LogFactory.getLog(HttpsForFraudDetection) + + @Override + WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) { + if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") { + int httpsPort = SocketUtils.findAvailableTcpPort() + log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server") + return httpStubConfiguration + .httpsPort(httpsPort) + } + return httpStubConfiguration + } + } +} +for the following configuration file: +stubrunner: + repositoryRoot: classpath:m2repo/repository/ + ids: + - org.springframework.cloud.contract.verifier.stubs:loanIssuance + - org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer + - org.springframework.cloud.contract.verifier.stubs:bootService + stubs-mode: remote +Instead of using the properties you can also use the properties inside the @AutoConfigureStubRunner. +Below you can find an example of achieving the same result by setting values on the annotation. +@AutoConfigureStubRunner( + ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance", + "org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer", + "org.springframework.cloud.contract.verifier.stubs:bootService"], + stubsMode = StubRunnerProperties.StubsMode.REMOTE, + repositoryRoot = "classpath:m2repo/repository/") +Stub Runner Spring registers environment variables in the following manner +for every registered WireMock server. Example for Stub Runner ids + com.example:foo, com.example:bar. + + +stubrunner.runningstubs.foo.port + + +stubrunner.runningstubs.com.example.foo.port + + +stubrunner.runningstubs.bar.port + + +stubrunner.runningstubs.com.example.bar.port + + +Which you can reference in your code. +You can also use the @StubRunnerPort annotation to inject the port of a running stub. +Value of the annotation can be the groupid:artifactid or just the artifactid. Example for Stub Runner ids +com.example:foo, com.example:bar. +@StubRunnerPort("foo") +int fooPort; +@StubRunnerPort("com.example:bar") +int barPort; +
    +
    +
    +Stub Runner Spring Cloud +Stub Runner can integrate with Spring Cloud. +For real life examples you can check the + + +producer app sample + + +consumer app sample + + +
    +Stubbing Service Discovery +The most important feature of Stub Runner Spring Cloud is the fact that it’s stubbing + + +DiscoveryClient + + +Ribbon ServerList + + +that means that regardless of the fact whether you’re using Zookeeper, Consul, Eureka or anything else, you don’t need that in your tests. +We’re starting WireMock instances of your dependencies and we’re telling your application whenever you’re using Feign, load balanced RestTemplate +or DiscoveryClient directly, to call those stubbed servers instead of calling the real Service Discovery tool. +For example this test will pass +def 'should make service discovery work'() { + expect: 'WireMocks are running' + "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance' + "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer' + and: 'Stubs can be reached via load service discovery' + restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance' + restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer' +} +for the following configuration file +stubrunner: + idsToServiceIds: + ivyNotation: someValueInsideYourCode + fraudDetectionServer: someNameThatShouldMapFraudDetectionServer +
    +Test profiles and service discovery +In your integration tests you typically don’t want to call neither a discovery service (e.g. Eureka) +or Config Server. That’s why you create an additional test configuration in which you want to disable +these features. +Due to certain limitations of spring-cloud-commons to achieve this you have disable these properties +via a static block like presented below (example for Eureka) + //Hack to work around https://github.com/spring-cloud/spring-cloud-commons/issues/156 + static { + System.setProperty("eureka.client.enabled", "false"); + System.setProperty("spring.cloud.config.failFast", "false"); + } +
    +
    +
    +Additional Configuration +You can match the artifactId of the stub with the name of your app by using the stubrunner.idsToServiceIds: map. +You can disable Stub Runner Ribbon support by providing: stubrunner.cloud.ribbon.enabled equal to false +You can disable Stub Runner support by providing: stubrunner.cloud.enabled equal to false + +By default all service discovery will be stubbed. That means that regardless of the fact if you have +an existing DiscoveryClient its results will be ignored. However, if you want to reuse it, just set + stubrunner.cloud.delegate.enabled to true and then your existing DiscoveryClient results will be + merged with the stubbed ones. + +The default Maven configuration used by Stub Runner can be tweaked either +via the following system properties or environment variables + + +maven.repo.local - path to the custom maven local repository location + + +org.apache.maven.user-settings - path to custom maven user settings location + + +org.apache.maven.global-settings - path to maven global settings location + + +
    +
    +
    +Stub Runner Boot Application +Spring Cloud Contract Stub Runner Boot is a Spring Boot application that exposes REST endpoints to +trigger the messaging labels and to access started WireMock servers. +One of the use-cases is to run some smoke (end to end) tests on a deployed application. +You can check out the Spring Cloud Pipelines +project for more information. +
    +How to use it? +
    +Stub Runner Server +Just add the +compile "org.springframework.cloud:spring-cloud-starter-stub-runner" +Annotate a class with @EnableStubRunnerServer, build a fat-jar and you’re ready to go! +For the properties check the Stub Runner Spring section. +
    +
    +Stub Runner Server Fat Jar +You can download a standalone JAR from Maven (e.g. for version 2.0.1.RELEASE), as follows: +$ wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.0.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.0.1.RELEASE.jar' +$ java -jar stub-runner.jar --stubrunner.ids=... --stubrunner.repositoryRoot=... +
    +
    +Spring Cloud CLI +Starting from 1.4.0.RELEASE version of the Spring Cloud CLI +project you can start Stub Runner Boot by executing spring cloud stubrunner. +In order to pass the configuration just create a stubrunner.yml file in the current working directory +or a subdirectory called config or in ~/.spring-cloud. The file could look like this +(example for running stubs installed locally) + +stubrunner.yml + +stubrunner: + stubsMode: LOCAL + ids: + - com.example:beer-api-producer:+:9876 + + +and then just call spring cloud stubrunner from your terminal window to start +the Stub Runner server. It will be available at port 8750. +
    +
    +
    +Endpoints +
    +HTTP + + +GET /stubs - returns a list of all running stubs in ivy:integer notation + + +GET /stubs/{ivy} - returns a port for the given ivy notation (when calling the endpoint ivy can also be artifactId only) + + +
    +
    +Messaging +For Messaging + + +GET /triggers - returns a list of all running labels in ivy : [ label1, label2 …​] notation + + +POST /triggers/{label} - executes a trigger with label + + +POST /triggers/{ivy}/{label} - executes a trigger with label for the given ivy notation (when calling the endpoint ivy can also be artifactId only) + + +
    +
    +
    +Example +@ContextConfiguration(classes = StubRunnerBoot, loader = SpringBootContextLoader) +@SpringBootTest(properties = "spring.cloud.zookeeper.enabled=false") +@ActiveProfiles("test") +class StubRunnerBootSpec extends Specification { + + @Autowired + StubRunning stubRunning + + def setup() { + RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), + new TriggerController(stubRunning)) + } + + def 'should return a list of running stub servers in "full ivy:port" notation'() { + when: + String response = RestAssuredMockMvc.get('/stubs').body.asString() + then: + def root = new JsonSlurper().parseText(response) + root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer + } + + def 'should return a port on which a [#stubId] stub is running'() { + when: + def response = RestAssuredMockMvc.get("/stubs/${stubId}") + then: + response.statusCode == 200 + Integer.valueOf(response.body.asString()) > 0 + where: + stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs', + 'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs', + 'org.springframework.cloud.contract.verifier.stubs:bootService:+', + 'org.springframework.cloud.contract.verifier.stubs:bootService', + 'bootService'] + } + + def 'should return 404 when missing stub was called'() { + when: + def response = RestAssuredMockMvc.get("/stubs/a:b:c:d") + then: + response.statusCode == 404 + } + + def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() { + when: + String response = RestAssuredMockMvc.get('/triggers').body.asString() + then: + def root = new JsonSlurper().parseText(response) + root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book", "return_book_1", "return_book_2"]) + } + + def 'should trigger a messaging label'() { + given: + StubRunning stubRunning = Mock() + RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning)) + when: + def response = RestAssuredMockMvc.post("/triggers/delete_book") + then: + response.statusCode == 200 + and: + 1 * stubRunning.trigger('delete_book') + } + + def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() { + given: + StubRunning stubRunning = Mock() + RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning)) + when: + def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book") + then: + response.statusCode == 200 + and: + 1 * stubRunning.trigger(stubId, 'delete_book') + where: + stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService'] + } + + def 'should throw exception when trigger is missing'() { + when: + RestAssuredMockMvc.post("/triggers/missing_label") + then: + Exception e = thrown(Exception) + e.message.contains("Exception occurred while trying to return [missing_label] label.") + e.message.contains("Available labels are") + e.message.contains("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]") + e.message.contains("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=") + } + +} +
    +
    +Stub Runner Boot with Service Discovery +One of the possibilities of using Stub Runner Boot is to use it as a feed of stubs for "smoke-tests". What does it mean? + Let’s assume that you don’t want to deploy 50 microservice to a test environment in order + to check if your application is working fine. You’ve already executed a suite of tests during the build process + but you would also like to ensure that the packaging of your application is fine. What you can do + is to deploy your application to an environment, start it and run a couple of tests on it to see if + it’s working fine. We can call those tests smoke-tests since their idea is to check only a handful + of testing scenarios. +The problem with this approach is such that if you’re doing microservices most likely you’re + using a service discovery tool. Stub Runner Boot allows you to solve this issue by starting the + required stubs and register them in a service discovery tool. Let’s take a look at an example of + such a setup with Eureka. Let’s assume that Eureka was already running. +@SpringBootApplication +@EnableStubRunnerServer +@EnableEurekaClient +@AutoConfigureStubRunner +public class StubRunnerBootEurekaExample { + + public static void main(String[] args) { + SpringApplication.run(StubRunnerBootEurekaExample.class, args); + } + +} +As you can see we want to start a Stub Runner Boot server @EnableStubRunnerServer, enable Eureka client @EnableEurekaClient +and we want to have the stub runner feature turned on @AutoConfigureStubRunner. +Now let’s assume that we want to start this application so that the stubs get automatically registered. + We can do it by running the app java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar where + ${SYSTEM_PROPS} would contain the following list of properties +* -Dstubrunner.repositoryRoot=https://repo.spring.io/snapshot (1) +* -Dstubrunner.cloud.stubbed.discovery.enabled=false (2) +* -Dstubrunner.ids=org.springframework.cloud.contract.verifier.stubs:loanIssuance,org. +* springframework.cloud.contract.verifier.stubs:fraudDetectionServer,org.springframework. +* cloud.contract.verifier.stubs:bootService (3) +* -Dstubrunner.idsToServiceIds.fraudDetectionServer= +* someNameThatShouldMapFraudDetectionServer (4) +* +* (1) - we tell Stub Runner where all the stubs reside (2) - we don't want the default +* behaviour where the discovery service is stubbed. That's why the stub registration will +* be picked (3) - we provide a list of stubs to download (4) - we provide a list of +That way your deployed application can send requests to started WireMock servers via the service +discovery. Most likely points 1-3 could be set by default in application.yml cause they are not +likely to change. That way you can provide only the list of stubs to download whenever you start +the Stub Runner Boot. +
    +
    +
    +Stubs Per Consumer +There are cases in which 2 consumers of the same endpoint want to have 2 different responses. + +This approach also allows you to immediately know which consumer is using which part of your API. +You can remove part of a response that your API produces and you can see which of your autogenerated tests +fails. If none fails then you can safely delete that part of the response cause nobody is using it. + +Let’s look at the following example for contract defined for the producer called producer. +There are 2 consumers: foo-consumer and bar-consumer. +Consumer foo-service +request { + url '/foo' + method GET() +} +response { + status OK() + body( + foo: "foo" + } +} +Consumer bar-service +request { + url '/foo' + method GET() +} +response { + status OK() + body( + bar: "bar" + } +} +You can’t produce for the same request 2 different responses. That’s why you can properly package the +contracts and then profit from the stubsPerConsumer feature. +On the producer side the consumers can have a folder that contains contracts related only to them. +By setting the stubrunner.stubs-per-consumer flag to true we no longer register all stubs but only those that +correspond to the consumer application’s name. In other words we’ll scan the path of every stub and +if it contains the subfolder with name of the consumer in the path only then will it get registered. +On the foo producer side the contracts would look like this +. +└── contracts + ├── bar-consumer + │   ├── bookReturnedForBar.groovy + │   └── shouldCallBar.groovy + └── foo-consumer + ├── bookReturnedForFoo.groovy + └── shouldCallFoo.groovy +Being the bar-consumer consumer you can either set the spring.application.name or the stubrunner.consumer-name to bar-consumer +Or set the test as follows: +@ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +@SpringBootTest(properties = ["spring.application.name=bar-consumer"]) +@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers", + repositoryRoot = "classpath:m2repo/repository/", + stubsMode = StubRunnerProperties.StubsMode.REMOTE, + stubsPerConsumer = true) +class StubRunnerStubsPerConsumerSpec extends Specification { +... +} +Then only the stubs registered under a path that contains the bar-consumer in its name (i.e. those from the +src/test/resources/contracts/bar-consumer/some/contracts/…​ folder) will be allowed to be referenced. +Or set the consumer name explicitly +@ContextConfiguration(classes = Config, loader = SpringBootContextLoader) +@SpringBootTest +@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers", + repositoryRoot = "classpath:m2repo/repository/", + consumerName = "foo-consumer", + stubsMode = StubRunnerProperties.StubsMode.REMOTE, + stubsPerConsumer = true) +class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification { +... +} +Then only the stubs registered under a path that contains the foo-consumer in its name (i.e. those from the +src/test/resources/contracts/foo-consumer/some/contracts/…​ folder) will be allowed to be referenced. +You can check out issue 224 for more +information about the reasons behind this change. +
    +
    +Common +This section briefly describes common properties, including: + + + + + + + + +
    +Common Properties for JUnit and Spring +You can set repetitive properties by using system properties or Spring configuration +properties. Here are their names with their default values: + + + + + + + +Property name +Default value +Description + + + + +stubrunner.minPort +10000 +Minimum value of a port for a started WireMock with stubs. + + +stubrunner.maxPort +15000 +Maximum value of a port for a started WireMock with stubs. + + +stubrunner.repositoryRoot + +Maven repo URL. If blank, then call the local maven repo. + + +stubrunner.classifier +stubs +Default classifier for the stub artifacts. + + +stubrunner.stubsMode +CLASSPATH +The way you want to fetch and register the stubs + + +stubrunner.ids + +Array of Ivy notation stubs to download. + + +stubrunner.username + +Optional username to access the tool that stores the JARs with +stubs. + + +stubrunner.password + +Optional password to access the tool that stores the JARs with +stubs. + + +stubrunner.stubsPerConsumer +false +Set to true if you want to use different stubs for +each consumer instead of registering all stubs for every consumer. + + +stubrunner.consumerName + +If you want to use a stub for each consumer and want to +override the consumer name just change this value. + + + + +
    +
    +Stub Runner Stubs IDs +You can provide the stubs to download via the stubrunner.ids system property. They +follow this pattern: +groupId:artifactId:version:classifier:port +Note that version, classifier and port are optional. + + +If you do not provide the port, a random one will be picked. + + +If you do not provide the classifier, the default is used. (Note that you can +pass an empty classifier this way: groupId:artifactId:version:). + + +If you do not provide the version, then the + will be passed and the latest one is +downloaded. + + +port means the port of the WireMock server. + +Starting with version 1.0.4, you can provide a range of versions that you +would like the Stub Runner to take into consideration. You can read more about the +Aether versioning +ranges here. + +
    +
    +
    +Stub Runner Docker +We’re publishing a spring-cloud/spring-cloud-contract-stub-runner Docker image +that will start the standalone version of Stub Runner. +If you want to learn more about the basics of Maven, artifact ids, +group ids, classifiers and Artifact Managers, just click here . +
    +How to use it +Just execute the docker image. You can pass any of the +as environment variables. The convention is that all the +letters should be upper case. The camel case notation should +and the dot (.) should be separated via underscore (_). E.g. + the stubrunner.repositoryRoot property should be represented + as a STUBRUNNER_REPOSITORY_ROOT environment variable. +
    +
    +Example of client side usage in a non JVM project +We’d like to use the stubs created in this step. +Let’s assume that we want to run the stubs on port 9876. The NodeJS code +is available here: +$ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs +$ cd bookstore +Let’s run the Stub Runner Boot application with the stubs. +# Provide the Spring Cloud Contract Docker version +$ SC_CONTRACT_DOCKER_VERSION="..." +# The IP at which the app is running and Docker container can reach it +$ APP_IP="192.168.0.100" +# Spring Cloud Contract Stub Runner properties +$ STUBRUNNER_PORT="8083" +# Stub coordinates 'groupId:artifactId:version:classifier:port' +$ STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876" +$ STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local" +# Run the docker with Stub Runner Boot +$ docker run --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" -e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" -e "STUBRUNNER_STUBS_MODE=REMOTE" -p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}" +What’s happening is that + + +a standalone Stub Runner application got started + + +it downloaded the stub with coordinates com.example:bookstore:0.0.1.RELEASE:stubs on port 9876 + + +it got downloaded from Artifactory running at http://192.168.0.100:8081/artifactory/libs-release-local + + +after a while Stub Runner will be running on port 8083 + + +and the stubs will be running at port 9876 + + +On the server side we built a stateful stub. Let’s use curl to assert +that the stubs are setup properly. +# let's execute the first request (no response is returned) +$ curl -H "Content-Type:application/json" -X POST --data '{ "title" : "Title", "genre" : "Genre", "description" : "Description", "author" : "Author", "publisher" : "Publisher", "pages" : 100, "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg", "buy_url" : "https://pivotal.io" }' http://localhost:9876/api/books +# Now time for the second request +$ curl -X GET http://localhost:9876/api/books +# You will receive contents of the JSON + +If you want use the stubs that you have built locally, on your host, +then you should pass the environment variable -e STUBRUNNER_STUBS_MODE=LOCAL and mount +the volume of your local m2 -v "${HOME}/.m2/:/root/.m2:ro" + +
    +
    +
    + +Stub Runner for Messaging +Stub Runner can run the published stubs in memory. It can integrate with the following +frameworks: + + +Spring Integration + + +Spring Cloud Stream + + +Apache Camel + + +Spring AMQP + + +It also provides entry points to integrate with any other solution on the market. + +If you have multiple frameworks on the classpath Stub Runner will need to +define which one should be used. Let’s assume that you have both AMQP, Spring Cloud Stream and Spring Integration +on the classpath. Then you need to set stubrunner.stream.enabled=false and stubrunner.integration.enabled=false. +That way the only remaining framework is Spring AMQP. + +
    +Stub triggering +To trigger a message, use the StubTrigger interface: +package org.springframework.cloud.contract.stubrunner; + +import java.util.Collection; +import java.util.Map; + +/** + * Contract for triggering stub messages. + * + * @author Marcin Grzejszczak + */ +public interface StubTrigger { + + /** + * Triggers an event by a given label for a given {@code groupid:artifactid} notation. + * You can use only {@code artifactId} too. + * + * Feature related to messaging. + * @param ivyNotation ivy notation of a stub + * @param labelName name of the label to trigger + * @return true - if managed to run a trigger + */ + boolean trigger(String ivyNotation, String labelName); + + /** + * Triggers an event by a given label. + * + * Feature related to messaging. + * @param labelName name of the label to trigger + * @return true - if managed to run a trigger + */ + boolean trigger(String labelName); + + /** + * Triggers all possible events. + * + * Feature related to messaging. + * @return true - if managed to run a trigger + */ + boolean trigger(); + + /** + * Feature related to messaging. + * @return a mapping of ivy notation of a dependency to all the labels it has. + */ + Map<String, Collection<String>> labels(); + +} +For convenience, the StubFinder interface extends StubTrigger, so you only need one +or the other in your tests. +StubTrigger gives you the following options to trigger a message: + + + + + + + + + + + + + + +
    +Trigger by Label +stubFinder.trigger('return_book_1') +
    +
    +Trigger by Group and Artifact Ids +stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1') +
    +
    +Trigger by Artifact Ids +stubFinder.trigger('streamService', 'return_book_1') +
    +
    +Trigger All Messages +stubFinder.trigger() +
    +
    +
    +Stub Runner Camel +Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to integrate with Apache Camel. +For the provided artifacts it will automatically download the stubs and register the required +routes. +
    +Adding it to the project +It’s enough to have both Apache Camel and Spring Cloud Contract Stub Runner on classpath. +Remember to annotate your test class with @AutoConfigureStubRunner. +
    +
    +Disabling the functionality +If you need to disable this functionality just pass stubrunner.camel.enabled=false property. +
    +
    +Examples +
    +Stubs structure +Let us assume that we have the following Maven repository with a deployed stubs for the +camelService application. +└── .m2 + └── repository + └── io + └── codearte + └── accurest + └── stubs + └── camelService + ├── 0.0.1-SNAPSHOT + │   ├── camelService-0.0.1-SNAPSHOT.pom + │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +And the stubs contain the following structure: +├── META-INF +│   └── MANIFEST.MF +└── repository + ├── accurest + │   ├── bookDeleted.groovy + │   ├── bookReturned1.groovy + │   └── bookReturned2.groovy + └── mappings +Let’s consider the following contracts (let' number it with 1): +Contract.make { + label 'return_book_1' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('jms:output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } +} +and number 2 +Contract.make { + label 'return_book_2' + input { + messageFrom('jms:input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('jms:output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } +} +
    +
    +Scenario 1 (no input message) +So as to trigger a message via the return_book_1 label we’ll use the StubTigger interface as follows +stubFinder.trigger('return_book_1') +Next we’ll want to listen to the output of the message sent to jms:output +Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000) +And the received message would pass the following assertions +receivedMessage != null +assertThatBodyContainsBookNameFoo(receivedMessage.in.body) +receivedMessage.in.headers.get('BOOK-NAME') == 'foo' +
    +
    +Scenario 2 (output triggered by input) +Since the route is set for you it’s enough to just send a message to the jms:output destination. +producerTemplate. + sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header']) +Next we’ll want to listen to the output of the message sent to jms:output +Exchange receivedMessage = consumerTemplate.receive('jms:output', 5000) +And the received message would pass the following assertions +receivedMessage != null +assertThatBodyContainsBookNameFoo(receivedMessage.in.body) +receivedMessage.in.headers.get('BOOK-NAME') == 'foo' +
    +
    +Scenario 3 (input with no output) +Since the route is set for you it’s enough to just send a message to the jms:output destination. +producerTemplate. + sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header']) +
    +
    +
    +
    +Stub Runner Integration +Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to +integrate with Spring Integration. For the provided artifacts, it automatically downloads +the stubs and registers the required routes. +
    +Adding the Runner to the Project +You can have both Spring Integration and Spring Cloud Contract Stub Runner on the +classpath. Remember to annotate your test class with @AutoConfigureStubRunner. +
    +
    +Disabling the functionality +If you need to disable this functionality, set the +stubrunner.integration.enabled=false property. +Assume that you have the following Maven repository with deployed stubs for the +integrationService application: +└── .m2 + └── repository + └── io + └── codearte + └── accurest + └── stubs + └── integrationService + ├── 0.0.1-SNAPSHOT + │   ├── integrationService-0.0.1-SNAPSHOT.pom + │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +Further assume the stubs contain the following structure: +├── META-INF +│   └── MANIFEST.MF +└── repository + ├── accurest + │   ├── bookDeleted.groovy + │   ├── bookReturned1.groovy + │   └── bookReturned2.groovy + └── mappings +Consider the following contracts (numbered 1): +Contract.make { + label 'return_book_1' + input { + triggeredBy('bookReturnedTriggered()') + } + outputMessage { + sentTo('output') + body('''{ "bookName" : "foo" }''') + headers { + header('BOOK-NAME', 'foo') + } + } +} +Now consider 2: +Contract.make { + label 'return_book_2' + input { + messageFrom('input') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } +} +and the following Spring Integration Route: +<?xml version="1.0" encoding="UTF-8"?> +<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:beans="http://www.springframework.org/schema/beans" + xmlns="http://www.springframework.org/schema/integration" + xsi:schemaLocation="http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/integration + http://www.springframework.org/schema/integration/spring-integration.xsd"> + + + <!-- REQUIRED FOR TESTING --> + <bridge input-channel="output" + output-channel="outputTest"/> + + <channel id="outputTest"> + <queue/> + </channel> + +</beans:beans> +These examples lend themselves to three scenarios: + + + + + + + + + + + +
    +Scenario 1 (no input message) +To trigger a message via the return_book_1 label, use the StubTigger interface, as +follows: +stubFinder.trigger('return_book_1') +To listen to the output of the message sent to output: +Message<?> receivedMessage = messaging.receive('outputTest') +The received message would pass the following assertions: +receivedMessage != null +assertJsons(receivedMessage.payload) +receivedMessage.headers.get('BOOK-NAME') == 'foo' +
    +
    +Scenario 2 (output triggered by input) +Since the route is set for you, you can send a message to the output +destination: +messaging.send(new BookReturned('foo'), [sample: 'header'], 'input') +To listen to the output of the message sent to output: +Message<?> receivedMessage = messaging.receive('outputTest') +The received message passes the following assertions: +receivedMessage != null +assertJsons(receivedMessage.payload) +receivedMessage.headers.get('BOOK-NAME') == 'foo' +
    +
    +Scenario 3 (input with no output) +Since the route is set for you, you can send a message to the input destination: +messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete') +
    +
    +
    +
    +Stub Runner Stream +Spring Cloud Contract Verifier Stub Runner’s messaging module gives you an easy way to +integrate with Spring Stream. For the provided artifacts, it automatically downloads the +stubs and registers the required routes. + +If Stub Runner’s integration with Stream the messageFrom or sentTo Strings +are resolved first as a destination of a channel and no such destination exists, the +destination is resolved as a channel name. + + +If you want to use Spring Cloud Stream remember, to add a dependency on +org.springframework.cloud:spring-cloud-stream-test-support. + + +Maven + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-stream-test-support</artifactId> + <scope>test</scope> +</dependency> + + + +Gradle + +testCompile "org.springframework.cloud:spring-cloud-stream-test-support" + + +
    +Adding the Runner to the Project +You can have both Spring Cloud Stream and Spring Cloud Contract Stub Runner on the +classpath. Remember to annotate your test class with @AutoConfigureStubRunner. +
    +
    +Disabling the functionality +If you need to disable this functionality, set the stubrunner.stream.enabled=false +property. +Assume that you have the following Maven repository with a deployed stubs for the +streamService application: +└── .m2 + └── repository + └── io + └── codearte + └── accurest + └── stubs + └── streamService + ├── 0.0.1-SNAPSHOT + │   ├── streamService-0.0.1-SNAPSHOT.pom + │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +Further assume the stubs contain the following structure: +├── META-INF +│   └── MANIFEST.MF +└── repository + ├── accurest + │   ├── bookDeleted.groovy + │   ├── bookReturned1.groovy + │   └── bookReturned2.groovy + └── mappings +Consider the following contracts (numbered 1): +Contract.make { + label 'return_book_1' + input { triggeredBy('bookReturnedTriggered()') } + outputMessage { + sentTo('returnBook') + body('''{ "bookName" : "foo" }''') + headers { header('BOOK-NAME', 'foo') } + } +} +Now consider 2: +Contract.make { + label 'return_book_2' + input { + messageFrom('bookStorage') + messageBody([ + bookName: 'foo' + ]) + messageHeaders { header('sample', 'header') } + } + outputMessage { + sentTo('returnBook') + body([ + bookName: 'foo' + ]) + headers { header('BOOK-NAME', 'foo') } + } +} +Now consider the following Spring configuration: +stubrunner.repositoryRoot: classpath:m2repo/repository/ +stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs +stubrunner.stubs-mode: remote +spring: + cloud: + stream: + bindings: + output: + destination: returnBook + input: + destination: bookStorage + +server: + port: 0 + +debug: true +These examples lend themselves to three scenarios: + + + + + + + + + + + +
    +Scenario 1 (no input message) +To trigger a message via the return_book_1 label, use the StubTrigger interface as +follows: +stubFinder.trigger('return_book_1') +To listen to the output of the message sent to a channel whose destination is +returnBook: +Message<?> receivedMessage = messaging.receive('returnBook') +The received message passes the following assertions: +receivedMessage != null +assertJsons(receivedMessage.payload) +receivedMessage.headers.get('BOOK-NAME') == 'foo' +
    +
    +Scenario 2 (output triggered by input) +Since the route is set for you, you can send a message to the bookStorage +destination: +messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage') +To listen to the output of the message sent to returnBook: +Message<?> receivedMessage = messaging.receive('returnBook') +The received message passes the following assertions: +receivedMessage != null +assertJsons(receivedMessage.payload) +receivedMessage.headers.get('BOOK-NAME') == 'foo' +
    +
    +Scenario 3 (input with no output) +Since the route is set for you, you can send a message to the output +destination: +messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete') +
    +
    +
    +
    +Stub Runner Spring AMQP +Spring Cloud Contract Verifier Stub Runner’s messaging module provides an easy way to +integrate with Spring AMQP’s Rabbit Template. For the provided artifacts, it +automatically downloads the stubs and registers the required routes. +The integration tries to work standalone (that is, without interaction with a running +RabbitMQ message broker). It expects a RabbitTemplate on the application context and +uses it as a spring boot test named @SpyBean. As a result, it can use the mockito spy +functionality to verify and inspect messages sent by the application. +On the message consumer side, the stub runner considers all @RabbitListener annotated +endpoints and all SimpleMessageListenerContainer objects on the application context. +As messages are usually sent to exchanges in AMQP, the message contract contains the +exchange name as the destination. Message listeners on the other side are bound to +queues. Bindings connect an exchange to a queue. If message contracts are triggered, the +Spring AMQP stub runner integration looks for bindings on the application context that +match this exchange. Then it collects the queues from the Spring exchanges and tries to +find message listeners bound to these queues. The message is triggered for all matching +message listeners. +If you need to work with routing keys, it’s enough to pass them via the amqp_receivedRoutingKey +messaging header. +
    +Adding the Runner to the Project +You can have both Spring AMQP and Spring Cloud Contract Stub Runner on the classpath and +set the property stubrunner.amqp.enabled=true. Remember to annotate your test class +with @AutoConfigureStubRunner. + +If you already have Stream and Integration on the classpath, you need +to disable them explicitly by setting the stubrunner.stream.enabled=false and +stubrunner.integration.enabled=false properties. + +Assume that you have the following Maven repository with a deployed stubs for the +spring-cloud-contract-amqp-test application. +└── .m2 + └── repository + └── com + └── example + └── spring-cloud-contract-amqp-test + ├── 0.4.0-SNAPSHOT + │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom + │   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar + │   └── maven-metadata-local.xml + └── maven-metadata-local.xml +Further assume that the stubs contain the following structure: +├── META-INF +│   └── MANIFEST.MF +└── contracts + └── shouldProduceValidPersonData.groovy +Consider the following contract: +Contract.make { + // Human readable description + description 'Should produce valid person data' + // Label by means of which the output message can be triggered + label 'contract-test.person.created.event' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('createPerson()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo 'contract-test.exchange' + headers { + header('contentType': 'application/json') + header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person') + } + // the body of the output message + body([ + id : $(consumer(9), producer(regex("[0-9]+"))), + name: "me" + ]) + } +} +Now consider the following Spring configuration: +stubrunner: + repositoryRoot: classpath:m2repo/repository/ + ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs + stubs-mode: remote + amqp: + enabled: true +server: + port: 0 +
    +Triggering the message +To trigger a message using the contract above, use the StubTrigger interface as +follows: +stubTrigger.trigger("contract-test.person.created.event") +The message has a destination of contract-test.exchange, so the Spring AMQP stub runner +integration looks for bindings related to this exchange. +@Bean +public Binding binding() { + return BindingBuilder.bind(new Queue("test.queue")) + .to(new DirectExchange("contract-test.exchange")).with("#"); +} +The binding definition binds the queue test.queue. As a result, the following listener +definition is matched and invoked with the contract message. +@Bean +public SimpleMessageListenerContainer simpleMessageListenerContainer( + ConnectionFactory connectionFactory, + MessageListenerAdapter listenerAdapter) { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.setQueueNames("test.queue"); + container.setMessageListener(listenerAdapter); + + return container; +} +Also, the following annotated listener matches and is invoked: +@RabbitListener(bindings = @QueueBinding(value = @Queue("test.queue"), exchange = @Exchange(value = "contract-test.exchange", ignoreDeclarationExceptions = "true"))) +public void handlePerson(Person person) { + this.person = person; +} + +The message is directly handed over to the onMessage method of the +MessageListener associated with the matching SimpleMessageListenerContainer. + +
    +
    +Spring AMQP Test Configuration +In order to avoid Spring AMQP trying to connect to a running broker during our tests +configure a mock ConnectionFactory. +To disable the mocked ConnectionFactory, set the following property: +stubrunner.amqp.mockConnection=false +stubrunner: + amqp: + mockConnection: false +
    +
    +
    +
    + +Contract DSL +Spring Cloud Contract supports out of the box 2 types of DSL. One written in +Groovy and one written in YAML. +If you decide to write the contract in Groovy, do not be alarmed if you have not used Groovy +before. Knowledge of the language is not really needed, as the Contract DSL uses only a +tiny subset of it (only literals, method calls and closures). Also, the DSL is statically +typed, to make it programmer-readable without any knowledge of the DSL itself. + +Remember that, inside the Groovy contract file, you have to provide the fully +qualified name to the Contract class and make static imports, such as +org.springframework.cloud.spec.Contract.make { …​ }. You can also provide an import to +the Contract class: import org.springframework.cloud.spec.Contract and then call +Contract.make { …​ }. + + +Spring Cloud Contract supports defining multiple contracts in a single file. + +The following is a complete example of a Groovy contract definition: + +The following is a complete example of a YAML contract definition: +description: Some description +name: some name +priority: 8 +ignored: true +request: + url: /foo + queryParameters: + a: b + b: c + method: PUT + headers: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +response: + status: 200 + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + nullValue: null + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + - path: $.nullValue + type: by_null + value: null + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) + +You can compile contracts to stubs mapping using standalone maven command: +mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert + +
    +Limitations + +Spring Cloud Contract Verifier does not properly support XML. Please use JSON or +help us implement this feature. + + +The support for verifying the size of JSON arrays is experimental. If you want +to turn it on, please set the value of the following system property to true: +spring.cloud.contract.verifier.assert.size. By default, this feature is set to false. +You can also provide the assertJsonSize property in the plugin configuration. + + +Because JSON structure can have any form, it can be impossible to parse it +properly when using the Groovy DSL and the value(consumer(…​), producer(…​)) notation in GString. That +is why you should use the Groovy Map notation. + +
    +
    +Common Top-Level elements +The following sections describe the most common top-level elements: + + + + + + + + + + + + + + + + + +
    +Description +You can add a description to your contract. The description is arbitrary text. The +following code shows an example: + +Groovy DSL + + org.springframework.cloud.contract.spec.Contract.make { + description(''' +given: + An input +when: + Sth happens +then: + Output +''') + } + + + +YAML + +description: Some description +name: some name +priority: 8 +ignored: true +request: + url: /foo + queryParameters: + a: b + b: c + method: PUT + headers: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +response: + status: 200 + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + nullValue: null + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + - path: $.nullValue + type: by_null + value: null + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) + + +
    +
    +Name +You can provide a name for your contract. Assume that you provided the following name: +should register a user. If you do so, the name of the autogenerated test is +validate_should_register_a_user. Also, the name of the stub in a WireMock stub is +should_register_a_user.json. + +You must ensure that the name does not contain any characters that make the +generated test not compile. Also, remember that, if you provide the same name for +multiple contracts, your autogenerated tests fail to compile and your generated stubs +override each other. + + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + name("some_special_name") +} + + + +YAML + +name: some name + + +
    +
    +Ignoring Contracts +If you want to ignore a contract, you can either set a value of ignored contracts in the +plugin configuration or set the ignored property on the contract itself: + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + ignored() +} + + + +YAML + +ignored: true + + +
    +
    +Passing Values from Files +Starting with version 1.2.0, you can pass values from files. Assume that you have the +following resources in our project. +└── src +    └── test +       └── resources +          └── contracts +    ├── readFromFile.groovy +    ├── request.json +    └── response.json +Further assume that your contract is as follows: + +Groovy DSL + +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method('PUT') + headers { + contentType(applicationJson()) + } + body(file("request.json")) + url("/1") + } + response { + status OK() + body(file("response.json")) + headers { + contentType(applicationJson()) + } + } +} + + + +YAML + +request: + method: GET + url: /foo + bodyFromFile: request.json +response: + status: 200 + bodyFromFile: response.json + + +Further assume that the JSON files is as follows: +request.json +{ + "status": "REQUEST" +} +response.json +{ + "status": "RESPONSE" +} +When test or stub generation takes place, the contents of the file is passed to the body +of a request or a response. The name of the file needs to be a file with location +relative to the folder in which the contract lays. +If you need to pass the contents of a file in a binary form +it’s enough for you to use the fileAsBytes method in Groovy DSL or bodyFromFileAsBytes field in YAML. + +Groovy DSL + +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + url("/1") + method(PUT()) + headers { + contentType(applicationOctetStream()) + } + body(fileAsBytes("request.pdf")) + } + response { + status 200 + body(fileAsBytes("response.pdf")) + headers { + contentType(applicationOctetStream()) + } + } +} + + + +YAML + +request: + url: /1 + method: PUT + headers: + Content-Type: application/octet-stream + bodyFromFileAsBytes: request.pdf +response: + status: 200 + bodyFromFileAsBytes: response.pdf + headers: + Content-Type: application/octet-stream + + + +You should use this approach whenever you want to work with binary payloads both for HTTP and messaging. + +
    +
    +HTTP Top-Level Elements +The following methods can be called in the top-level closure of a contract definition. +request and response are mandatory. priority is optional. + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + // Definition of HTTP request part of the contract + // (this can be a valid request or invalid depending + // on type of contract being specified). + request { + method GET() + url "/foo" + //... + } + + // Definition of HTTP response part of the contract + // (a service implementing this contract should respond + // with following response after receiving request + // specified in "request" part above). + response { + status 200 + //... + } + + // Contract priority, which can be used for overriding + // contracts (1 is highest). Priority is optional. + priority 1 +} + + + +YAML + +priority: 8 +request: +... +response: +... + + + +If you want to make your contract have a higher value of priority +you need to pass a lower number to the priority tag / method. E.g. priority with +value 5 has higher priority than priority with value 10. + +
    +
    +
    +Request +The HTTP protocol requires only method and url to be specified in a request. The +same information is mandatory in request definition of the Contract. + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + // HTTP request method (GET/POST/PUT/DELETE). + method 'GET' + + // Path component of request URL is specified as follows. + urlPath('/users') + } + + response { + //... + status 200 + } +} + + + +YAML + +method: PUT +url: /foo + + +It is possible to specify an absolute rather than relative url, but using urlPath is +the recommended way, as doing so makes the tests host-independent. + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + + // Specifying `url` and `urlPath` in one contract is illegal. + url('http://localhost:8888/users') + } + + response { + //... + status 200 + } +} + + + +YAML + +request: + method: PUT + urlPath: /foo + + +request may contain query parameters. + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + //... + method GET() + + urlPath('/users') { + + // Each parameter is specified in form + // `'paramName' : paramValue` where parameter value + // may be a simple literal or one of matcher functions, + // all of which are used in this example. + queryParameters { + + // If a simple literal is used as value + // default matcher function is used (equalTo) + parameter 'limit': 100 + + // `equalTo` function simply compares passed value + // using identity operator (==). + parameter 'filter': equalTo("email") + + // `containing` function matches strings + // that contains passed substring. + parameter 'gender': value(consumer(containing("[mf]")), producer('mf')) + + // `matching` function tests parameter + // against passed regular expression. + parameter 'offset': value(consumer(matching("[0-9]+")), producer(123)) + + // `notMatching` functions tests if parameter + // does not match passed regular expression. + parameter 'loginStartsWith': value(consumer(notMatching(".{0,2}")), producer(3)) + } + } + + //... + } + + response { + //... + status 200 + } +} + + + +YAML + +request: +... + queryParameters: + a: b + b: c + headers: + foo: bar + fooReq: baz + cookies: + foo: bar + fooReq: baz + body: + foo: bar + matchers: + body: + - path: $.foo + type: by_regex + value: bar + headers: + - key: foo + regex: bar +response: + status: 200 + fixedDelayMilliseconds: 1000 + headers: + foo2: bar + foo3: foo33 + fooRes: baz + body: + foo2: bar + foo3: baz + nullValue: null + matchers: + body: + - path: $.foo2 + type: by_regex + value: bar + - path: $.foo3 + type: by_command + value: executeMe($it) + - path: $.nullValue + type: by_null + value: null + headers: + - key: foo2 + regex: bar + - key: foo3 + command: andMeToo($it) + cookies: + - key: foo2 + regex: bar + - key: foo3 + predefined: + + +request may contain additional request headers, as shown in the following example: + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + //... + method GET() + url "/foo" + + // Each header is added in form `'Header-Name' : 'Header-Value'`. + // there are also some helper methods + headers { + header 'key': 'value' + contentType(applicationJson()) + } + + //... + } + + response { + //... + status 200 + } +} + + + +YAML + +request: +... +headers: + foo: bar + fooReq: baz + + +request may contain additional request cookies, as shown in the following example: + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + //... + method GET() + url "/foo" + + // Each Cookies is added in form `'Cookie-Key' : 'Cookie-Value'`. + // there are also some helper methods + cookies { + cookie 'key': 'value' + cookie('another_key', 'another_value') + } + + //... + } + + response { + //... + status 200 + } +} + + + +YAML + +request: +... +cookies: + foo: bar + fooReq: baz + + +request may contain a request body: + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + //... + method GET() + url "/foo" + + // Currently only JSON format of request body is supported. + // Format will be determined from a header or body's content. + body '''{ "login" : "john", "name": "John The Contract" }''' + } + + response { + //... + status 200 + } +} + + + +YAML + +request: +... +body: + foo: bar + + +request may contain multipart elements. To include multipart elements, use the +multipart method/section, as shown in the following examples + +Groovy DSL + + + + + +YAML + +request: + method: PUT + url: /multipart + headers: + Content-Type: multipart/form-data;boundary=AaB03x + multipart: + params: + # key (parameter name), value (parameter value) pair + formParameter: '"formParameterValue"' + someBooleanParameter: true + named: + - paramName: file + fileName: filename.csv + fileContent: file content + matchers: + multipart: + params: + - key: formParameter + regex: ".+" + - key: someBooleanParameter + predefined: any_boolean + named: + - paramName: file + fileName: + predefined: non_empty + fileContent: + predefined: non_empty +response: + status: 200 + + +In the preceding example, we define parameters in either of two ways: + +Groovy DSL + +Directly, by using the map notation, where the value can be a dynamic property (such as +formParameter: $(consumer(…​), producer(…​))). + + +By using the named(…​) method that lets you set a named parameter. A named parameter +can set a name and content. You can call it either via a method with two arguments, +such as named("fileName", "fileContent"), or via a map notation, such as +named(name: "fileName", content: "fileContent"). + + + +YAML + +The multipart parameters are set via multipart.params section + + +The named parameters (the fileName and fileContent for a given parameter name) +can be set via the multipart.named section. That section contains +the paramName (name of the parameter), fileName (name of the file), +fileContent (content of the file) fields + + +The dynamic bits can be set via the matchers.multipart section + + +for parameters use the params section that can accept +regex or a predefined regular expression + + +for named params use the named section where first you +define the parameter name via paramName and then you can pass the +parametrization of either fileName or fileContent via +regex or a predefined regular expression + + + + +From this contract, the generated test is as follows: +// given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "multipart/form-data;boundary=AaB03x") + .param("formParameter", "\"formParameterValue\"") + .param("someBooleanParameter", "true") + .multiPart("file", "filename.csv", "file content".getBytes()); + +// when: + ResponseOptions response = given().spec(request) + .put("/multipart"); + +// then: + assertThat(response.statusCode()).isEqualTo(200); +The WireMock stub is as follows: + ''' +{ + "request" : { + "url" : "/multipart", + "method" : "PUT", + "headers" : { + "Content-Type" : { + "matches" : "multipart/form-data;boundary=AaB03x.*" + } + }, + "bodyPatterns" : [ { + "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"formParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n\\".+\\"\\r\\n--\\\\1.*" + }, { + "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"someBooleanParameter\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n(true|false)\\r\\n--\\\\1.*" + }, { + "matches" : ".*--(.*)\\r\\nContent-Disposition: form-data; name=\\"file\\"; filename=\\"[\\\\S\\\\s]+\\"\\r\\n(Content-Type: .*\\r\\n)?(Content-Transfer-Encoding: .*\\r\\n)?(Content-Length: \\\\d+\\r\\n)?\\r\\n[\\\\S\\\\s]+\\r\\n--\\\\1.*" + } ] + }, + "response" : { + "status" : 200, + "transformers" : [ "response-template", "foo-transformer" ] + } +} + ''' +
    +
    +Response +The response must contain an HTTP status code and may contain other information. The +following code shows an example: + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + //... + method GET() + url "/foo" + } + response { + // Status code sent by the server + // in response to request specified above. + status OK() + } +} + + + +YAML + +response: +... +status: 200 + + +Besides status, the response may contain headers, cookies and a body, both of which are +specified the same way as in the request (see the previous paragraph). + +Via the Groovy DSL you can reference the org.springframework.cloud.contract.spec.internal.HttpStatus +methods to provide a meaningful status instead of a digit. E.g. you can call +OK() for a status 200 or BAD_REQUEST() for 400. + +
    +
    +Dynamic properties +The contract can contain some dynamic properties: timestamps, IDs, and so on. You do not +want to force the consumers to stub their clocks to always return the same value of time +so that it gets matched by the stub. +For Groovy DSL you can provide the dynamic parts in your contracts +in two ways: pass them directly in the body or set them in a separate section called +bodyMatchers. + +Before 2.0.0 these were set using testMatchers and stubMatchers, +check out the migration guide for more information. + +For YAML you can only use the matchers section. +
    +Dynamic properties inside the body + +This section is valid only for Groovy DSL. Check out the + section for YAML examples of a similar feature. + +You can set the properties inside the body either with the value method or, if you use +the Groovy map notation, with $(). The following example shows how to set dynamic +properties with the value method: +value(consumer(...), producer(...)) +value(c(...), p(...)) +value(stub(...), test(...)) +value(client(...), server(...)) +The following example shows how to set dynamic properties with $(): +$(consumer(...), producer(...)) +$(c(...), p(...)) +$(stub(...), test(...)) +$(client(...), server(...)) +Both approaches work equally well. stub and client methods are aliases over the consumer +method. Subsequent sections take a closer look at what you can do with those values. +
    +
    +Regular expressions + +This section is valid only for Groovy DSL. Check out the + section for YAML examples of a similar feature. + +You can use regular expressions to write your requests in Contract DSL. Doing so is +particularly useful when you want to indicate that a given response should be provided +for requests that follow a given pattern. Also, you can use regular expressions when you +need to use patterns and not exact values both for your test and your server side tests. +The following example shows how to use regular expressions to write a request: +org.springframework.cloud.contract.spec.Contract.make { + request { + method('GET') + url $(consumer(~/\/[0-9]{2}/), producer('/12')) + } + response { + status OK() + body( + id: $(anyNumber()), + surname: $( + consumer('Kowalsky'), + producer(regex('[a-zA-Z]+')) + ), + name: 'Jan', + created: $(consumer('2014-02-02 12:23:43'), producer(execute('currentDate(it)'))), + correlationId: value(consumer('5d1f9fef-e0dc-4f3d-a7e4-72d2220dd827'), + producer(regex('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')) + ) + ) + headers { + header 'Content-Type': 'text/plain' + } + } +} +You can also provide only one side of the communication with a regular expression. If you +do so, then the contract engine automatically provides the generated string that matches +the provided regular expression. The following code shows an example: +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url value(consumer(regex('/foo/[0-9]{5}'))) + body([ + requestElement: $(consumer(regex('[0-9]{5}'))) + ]) + headers { + header('header', $(consumer(regex('application\\/vnd\\.fraud\\.v1\\+json;.*')))) + } + } + response { + status OK() + body([ + responseElement: $(producer(regex('[0-9]{7}'))) + ]) + headers { + contentType("application/vnd.fraud.v1+json") + } + } +} +In the preceding example, the opposite side of the communication has the respective data +generated for request and response. +Spring Cloud Contract comes with a series of predefined regular expressions that you can +use in your contracts, as shown in the following example: +protected static final Pattern TRUE_OR_FALSE = Pattern.compile(/(true|false)/) +protected static final Pattern ALPHA_NUMERIC = Pattern.compile('[a-zA-Z0-9]+') +protected static final Pattern ONLY_ALPHA_UNICODE = Pattern.compile(/[\p{L}]*/) +protected static final Pattern NUMBER = Pattern.compile('-?(\\d*\\.\\d+|\\d+)') +protected static final Pattern INTEGER = Pattern.compile('-?(\\d+)') +protected static final Pattern POSITIVE_INT = Pattern.compile('([1-9]\\d*)') +protected static final Pattern DOUBLE = Pattern.compile('-?(\\d*\\.\\d+)') +protected static final Pattern HEX = Pattern.compile('[a-fA-F0-9]+') +protected static final Pattern IP_ADDRESS = Pattern. + compile('([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])') +protected static final Pattern HOSTNAME_PATTERN = Pattern. + compile('((http[s]?|ftp):/)/?([^:/\\s]+)(:[0-9]{1,5})?') +protected static final Pattern EMAIL = Pattern. + compile('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}') +protected static final Pattern URL = UrlHelper.URL +protected static final Pattern HTTPS_URL = UrlHelper.HTTPS_URL +protected static final Pattern UUID = Pattern. + compile('[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}') +protected static final Pattern ANY_DATE = Pattern. + compile('(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])') +protected static final Pattern ANY_DATE_TIME = Pattern. + compile('([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])') +protected static final Pattern ANY_TIME = Pattern. + compile('(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])') +protected static final Pattern NON_EMPTY = Pattern.compile(/[\S\s]+/) +protected static final Pattern NON_BLANK = Pattern.compile(/^\s*\S[\S\s]*/) +protected static final Pattern ISO8601_WITH_OFFSET = Pattern. + compile(/([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.\d{1,6})?(Z|[+-][01]\d:[0-5]\d)/) + +protected static Pattern anyOf(String... values) { + return Pattern.compile(values.collect({ "^$it\$" }).join("|")) +} + +RegexProperty onlyAlphaUnicode() { + return new RegexProperty(ONLY_ALPHA_UNICODE).asString() +} + +RegexProperty alphaNumeric() { + return new RegexProperty(ALPHA_NUMERIC).asString() +} + +RegexProperty number() { + return new RegexProperty(NUMBER).asDouble() +} + +RegexProperty positiveInt() { + return new RegexProperty(POSITIVE_INT).asInteger() +} + +RegexProperty anyBoolean() { + return new RegexProperty(TRUE_OR_FALSE).asBooleanType() +} + +RegexProperty anInteger() { + return new RegexProperty(INTEGER).asInteger() +} + +RegexProperty aDouble() { + return new RegexProperty(DOUBLE).asDouble() +} + +RegexProperty ipAddress() { + return new RegexProperty(IP_ADDRESS).asString() +} + +RegexProperty hostname() { + return new RegexProperty(HOSTNAME_PATTERN).asString() +} + +RegexProperty email() { + return new RegexProperty(EMAIL).asString() +} + +RegexProperty url() { + return new RegexProperty(URL).asString() +} + +RegexProperty httpsUrl() { + return new RegexProperty(HTTPS_URL).asString() +} + +RegexProperty uuid() { + return new RegexProperty(UUID).asString() +} + +RegexProperty isoDate() { + return new RegexProperty(ANY_DATE).asString() +} + +RegexProperty isoDateTime() { + return new RegexProperty(ANY_DATE_TIME).asString() +} + +RegexProperty isoTime() { + return new RegexProperty(ANY_TIME).asString() +} + +RegexProperty iso8601WithOffset() { + return new RegexProperty(ISO8601_WITH_OFFSET).asString() +} + +RegexProperty nonEmpty() { + return new RegexProperty(NON_EMPTY).asString() +} + +RegexProperty nonBlank() { + return new RegexProperty(NON_BLANK).asString() +} +In your contract, you can use it as shown in the following example: +Contract dslWithOptionalsInString = Contract.make { + priority 1 + request { + method POST() + url '/users/password' + headers { + contentType(applicationJson()) + } + body( + email: $(consumer(optional(regex(email()))), producer('abc@abc.com')), + callback_url: $(consumer(regex(hostname())), producer('http://partners.com')) + ) + } + response { + status 404 + headers { + contentType(applicationJson()) + } + body( + code: value(consumer("123123"), producer(optional("123123"))), + message: "User not found by email = [${value(producer(regex(email())), consumer('not.existing@user.com'))}]" + ) + } +} +To make matters even simpler you can use a set of predefined objects that will automatically assume that you want a regular expression to be passed. +All of those methods start with any prefix: +T anyAlphaUnicode() + +T anyAlphaNumeric() + +T anyNumber() + +T anyInteger() + +T anyPositiveInt() + +T anyDouble() + +T anyHex() + +T aBoolean() + +T anyIpAddress() + +T anyHostname() + +T anyEmail() + +T anyUrl() + +T anyHttpsUrl() + +T anyUuid() + +T anyDate() + +T anyDateTime() + +T anyTime() + +T anyIso8601WithOffset() + +T anyNonBlankString() + +T anyNonEmptyString() + +T anyOf(String... values) +and this is an example of how you can reference those methods: +Contract contractDsl = Contract.make { + label 'trigger_event' + input { + triggeredBy('toString()') + } + outputMessage { + sentTo 'topic.rateablequote' + body([ + alpha : $(anyAlphaUnicode()), + number : $(anyNumber()), + anInteger : $(anyInteger()), + positiveInt : $(anyPositiveInt()), + aDouble : $(anyDouble()), + aBoolean : $(aBoolean()), + ip : $(anyIpAddress()), + hostname : $(anyHostname()), + email : $(anyEmail()), + url : $(anyUrl()), + httpsUrl : $(anyHttpsUrl()), + uuid : $(anyUuid()), + date : $(anyDate()), + dateTime : $(anyDateTime()), + time : $(anyTime()), + iso8601WithOffset: $(anyIso8601WithOffset()), + nonBlankString : $(anyNonBlankString()), + nonEmptyString : $(anyNonEmptyString()), + anyOf : $(anyOf('foo', 'bar')) + ]) + } +} +
    +
    +Passing Optional Parameters + +This section is valid only for Groovy DSL. Check out the + section for YAML examples of a similar feature. + +It is possible to provide optional parameters in your contract. However, you can provide +optional parameters only for the following: + + +STUB side of the Request + + +TEST side of the Response + + +The following example shows how to provide optional parameters: +org.springframework.cloud.contract.spec.Contract.make { + priority 1 + request { + method 'POST' + url '/users/password' + headers { + contentType(applicationJson()) + } + body( + email: $(consumer(optional(regex(email()))), producer('abc@abc.com')), + callback_url: $(consumer(regex(hostname())), producer('https://partners.com')) + ) + } + response { + status 404 + headers { + header 'Content-Type': 'application/json' + } + body( + code: value(consumer("123123"), producer(optional("123123"))) + ) + } +} +By wrapping a part of the body with the optional() method, you create a regular +expression that must be present 0 or more times. +If you use Spock for, the following test would be generated from the previous example: + """ + given: + def request = given() + .header("Content-Type", "application/json") + .body('''{"email":"abc@abc.com","callback_url":"https://partners.com"}''') + + when: + def response = given().spec(request) + .post("/users/password") + + then: + response.statusCode == 404 + response.header('Content-Type') == 'application/json' + and: + DocumentContext parsedJson = JsonPath.parse(response.body.asString()) + assertThatJson(parsedJson).field("['code']").matches("(123123)?") +""" +The following stub would also be generated: + ''' +{ + "request" : { + "url" : "/users/password", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.['email'] =~ /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,6})?/)]" + }, { + "matchesJsonPath" : "$[?(@.['callback_url'] =~ /((http[s]?|ftp):\\\\/)\\\\/?([^:\\\\/\\\\s]+)(:[0-9]{1,5})?/)]" + } ], + "headers" : { + "Content-Type" : { + "equalTo" : "application/json" + } + } + }, + "response" : { + "status" : 404, + "body" : "{\\"code\\":\\"123123\\",\\"message\\":\\"User not found by email == [not.existing@user.com]\\"}", + "headers" : { + "Content-Type" : "application/json" + } + }, + "priority" : 1 +} +''' +
    +
    +Executing Custom Methods on the Server Side + +This section is valid only for Groovy DSL. Check out the + section for YAML examples of a similar feature. + +You can define a method call that executes on the server side during the test. Such a +method can be added to the class defined as "baseClassForTests" in the configuration. The +following code shows an example of the contract portion of the test case: +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url $(consumer(regex('^/api/[0-9]{2}$')), producer('/api/12')) + headers { + header 'Content-Type': 'application/json' + } + body '''\ + [{ + "text": "Gonna see you at Warsaw" + }] + ''' + } + response { + body( + path: $(consumer('/api/12'), producer(regex('^/api/[0-9]{2}$'))), + correlationId: $(consumer('1223456'), producer(execute('isProperCorrelationId($it)'))) + ) + status OK() + } +} +The following code shows the base class portion of the test case: +abstract class BaseMockMvcSpec extends Specification { + + def setup() { + RestAssuredMockMvc.standaloneSetup(new PairIdController()) + } + + void isProperCorrelationId(Integer correlationId) { + assert correlationId == 123456 + } + + void isEmpty(String value) { + assert value == null + } + +} + +You cannot use both a String and execute to perform concatenation. For +example, calling header('Authorization', 'Bearer ' + execute('authToken()')) leads to +improper results. Instead, call header('Authorization', execute('authToken()')) and +ensure that the authToken() method returns everything you need. + +The type of the object read from the JSON can be one of the following, depending on the +JSON path: + + +String: If you point to a String value in the JSON. + + +JSONArray: If you point to a List in the JSON. + + +Map: If you point to a Map in the JSON. + + +Number: If you point to Integer, Double etc. in the JSON. + + +Boolean: If you point to a Boolean in the JSON. + + +In the request part of the contract, you can specify that the body should be taken from +a method. + +You must provide both the consumer and the producer side. The execute part +is applied for the whole body - not for parts of it. + +The following example shows how to read an object from JSON: +Contract contractDsl = Contract.make { + request { + method 'GET' + url '/something' + body( + $(c('foo'), p(execute('hashCode()'))) + ) + } + response { + status OK() + } +} +The preceding example results in calling the hashCode() method in the request body. +It should resemble the following code: +// given: + MockMvcRequestSpecification request = given() + .body(hashCode()); + +// when: + ResponseOptions response = given().spec(request) + .get("/something"); + +// then: + assertThat(response.statusCode()).isEqualTo(200); +
    +
    +Referencing the Request from the Response +The best situation is to provide fixed values, but sometimes you need to reference a +request in your response. +If you’re writing contracts using Groovy DSL, you can use the fromRequest() method, which lets +you reference a bunch of elements from the HTTP request. You can use the following +options: + + +fromRequest().url(): Returns the request URL and query parameters. + + +fromRequest().query(String key): Returns the first query parameter with a given name. + + +fromRequest().query(String key, int index): Returns the nth query parameter with a +given name. + + +fromRequest().path(): Returns the full path. + + +fromRequest().path(int index): Returns the nth path element. + + +fromRequest().header(String key): Returns the first header with a given name. + + +fromRequest().header(String key, int index): Returns the nth header with a given name. + + +fromRequest().body(): Returns the full request body. + + +fromRequest().body(String jsonPath): Returns the element from the request that +matches the JSON Path. + + +If you’re using the YAML contract definition you have to use the +Handlebars {{{ }}} notation with custom, Spring Cloud Contract + functions to achieve this. + + +{{{ request.url }}}: Returns the request URL and query parameters. + + +{{{ request.query.key.[index] }}}: Returns the nth query parameter with a given name. +E.g. for key foo, first entry {{{ request.query.foo.[0] }}} + + +{{{ request.path }}}: Returns the full path. + + +{{{ request.path.[index] }}}: Returns the nth path element. E.g. +for first entry `{{{ request.path.[0] }}} + + +{{{ request.headers.key }}}: Returns the first header with a given name. + + +{{{ request.headers.key.[index] }}}: Returns the nth header with a given name. + + +{{{ request.body }}}: Returns the full request body. + + +{{{ jsonpath this 'your.json.path' }}}: Returns the element from the request that +matches the JSON Path. E.g. for json path $.foo - {{{ jsonpath this '$.foo' }}} + + +Consider the following contract: + +Groovy DSL + + + + + +YAML + +request: + method: GET + url: /api/v1/xxxx + queryParameters: + foo: + - bar + - bar2 + headers: + Authorization: + - secret + - secret2 + body: + foo: bar + baz: 5 +response: + status: 200 + headers: + Authorization: "foo {{{ request.headers.Authorization.0 }}} bar" + body: + url: "{{{ request.url }}}" + path: "{{{ request.path }}}" + pathIndex: "{{{ request.path.1 }}}" + param: "{{{ request.query.foo }}}" + paramIndex: "{{{ request.query.foo.1 }}}" + authorization: "{{{ request.headers.Authorization.0 }}}" + authorization2: "{{{ request.headers.Authorization.1 }}" + fullBody: "{{{ request.body }}}" + responseFoo: "{{{ jsonpath this '$.foo' }}}" + responseBaz: "{{{ jsonpath this '$.baz' }}}" + responseBaz2: "Bla bla {{{ jsonpath this '$.foo' }}} bla bla" + + +Running a JUnit test generation leads to a test that resembles the following example: +// given: + MockMvcRequestSpecification request = given() + .header("Authorization", "secret") + .header("Authorization", "secret2") + .body("{\"foo\":\"bar\",\"baz\":5}"); + +// when: + ResponseOptions response = given().spec(request) + .queryParam("foo","bar") + .queryParam("foo","bar2") + .get("/api/v1/xxxx"); + +// then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Authorization")).isEqualTo("foo secret bar"); +// and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("['fullBody']").isEqualTo("{\"foo\":\"bar\",\"baz\":5}"); + assertThatJson(parsedJson).field("['authorization']").isEqualTo("secret"); + assertThatJson(parsedJson).field("['authorization2']").isEqualTo("secret2"); + assertThatJson(parsedJson).field("['path']").isEqualTo("/api/v1/xxxx"); + assertThatJson(parsedJson).field("['param']").isEqualTo("bar"); + assertThatJson(parsedJson).field("['paramIndex']").isEqualTo("bar2"); + assertThatJson(parsedJson).field("['pathIndex']").isEqualTo("v1"); + assertThatJson(parsedJson).field("['responseBaz']").isEqualTo(5); + assertThatJson(parsedJson).field("['responseFoo']").isEqualTo("bar"); + assertThatJson(parsedJson).field("['url']").isEqualTo("/api/v1/xxxx?foo=bar&foo=bar2"); + assertThatJson(parsedJson).field("['responseBaz2']").isEqualTo("Bla bla bar bla bla"); +As you can see, elements from the request have been properly referenced in the response. +The generated WireMock stub should resemble the following example: +{ + "request" : { + "urlPath" : "/api/v1/xxxx", + "method" : "POST", + "headers" : { + "Authorization" : { + "equalTo" : "secret2" + } + }, + "queryParameters" : { + "foo" : { + "equalTo" : "bar2" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.['baz'] == 5)]" + }, { + "matchesJsonPath" : "$[?(@.['foo'] == 'bar')]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"authorization\":\"{{{request.headers.Authorization.[0]}}}\",\"path\":\"{{{request.path}}}\",\"responseBaz\":{{{jsonpath this '$.baz'}}} ,\"param\":\"{{{request.query.foo.[0]}}}\",\"pathIndex\":\"{{{request.path.[1]}}}\",\"responseBaz2\":\"Bla bla {{{jsonpath this '$.foo'}}} bla bla\",\"responseFoo\":\"{{{jsonpath this '$.foo'}}}\",\"authorization2\":\"{{{request.headers.Authorization.[1]}}}\",\"fullBody\":\"{{{escapejsonbody}}}\",\"url\":\"{{{request.url}}}\",\"paramIndex\":\"{{{request.query.foo.[1]}}}\"}", + "headers" : { + "Authorization" : "{{{request.headers.Authorization.[0]}}};foo" + }, + "transformers" : [ "response-template" ] + } +} +Sending a request such as the one presented in the request part of the contract results +in sending the following response body: +{ + "url" : "/api/v1/xxxx?foo=bar&foo=bar2", + "path" : "/api/v1/xxxx", + "pathIndex" : "v1", + "param" : "bar", + "paramIndex" : "bar2", + "authorization" : "secret", + "authorization2" : "secret2", + "fullBody" : "{\"foo\":\"bar\",\"baz\":5}", + "responseFoo" : "bar", + "responseBaz" : 5, + "responseBaz2" : "Bla bla bar bla bla" +} + +This feature works only with WireMock having a version greater than or equal +to 2.5.1. The Spring Cloud Contract Verifier uses WireMock’s +response-template response transformer. It uses Handlebars to convert the Mustache {{{ }}} templates into +proper values. Additionally, it registers two helper functions: + + + +escapejsonbody: Escapes the request body in a format that can be embedded in a JSON. + + +jsonpath: For a given parameter, find an object in the request body. + + +
    +
    +Registering Your Own WireMock Extension +WireMock lets you register custom extensions. By default, Spring Cloud Contract registers +the transformer, which lets you reference a request from a response. If you want to +provide your own extensions, you can register an implementation of the +org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions interface. +Since we use the spring.factories extension approach, you can create an entry in +META-INF/spring.factories file similar to the following: +org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions=\ +org.springframework.cloud.contract.stubrunner.provider.wiremock.TestWireMockExtensions +org.springframework.cloud.contract.spec.ContractConverter=\ +org.springframework.cloud.contract.stubrunner.TestCustomYamlContractConverter +The following is an example of a custom extension: + +TestWireMockExtensions.groovy + +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.dsl.wiremock + +import com.github.tomakehurst.wiremock.extension.Extension + +/** + * Extension that registers the default transformer and the custom one + */ +class TestWireMockExtensions implements WireMockExtensions { + @Override + List<Extension> extensions() { + return [ + new DefaultResponseTransformer(), + new CustomExtension() + ] + } +} + +class CustomExtension implements Extension { + + @Override + String getName() { + return "foo-transformer" + } +} + + + +Remember to override the applyGlobally() method and set it to false if you +want the transformation to be applied only for a mapping that explicitly requires it. + +
    +
    +Dynamic Properties in the Matchers Sections +If you work with Pact, the following discussion may seem familiar. +Quite a few users are used to having a separation between the body and setting the +dynamic parts of a contract. +You can use the bodyMatchers section for two reasons: + + +Define the dynamic values that should end up in a stub. +You can set it in the request or inputMessage part of your contract. + + +Verify the result of your test. +This section is present in the response or outputMessage side of the +contract. + + +Currently, Spring Cloud Contract Verifier supports only JSON Path-based matchers with the +following matching possibilities: + +Groovy DSL + +For the stubs(in tests on the Consumer’s side): + + +byEquality(): The value taken from the consumer’s request via the provided JSON Path must be +equal to the value provided in the contract. + + +byRegex(…​): The value taken from the consumer’s request via the provided JSON Path must +match the regex. You can also pass the type of the expected matched value (e.g. asString(), asLong() etc.) + + +byDate(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO Date value. + + +byTimestamp(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO DateTime value. + + +byTime(): The value taken from the consumer’s request via the provided JSON Path must +match the regex for an ISO Time value. + + + + +For the verification(in generated tests on the Producer’s side): + + +byEquality(): The value taken from the producer’s response via the provided JSON Path must be +equal to the provided value in the contract. + + +byRegex(…​): The value taken from the producer’s response via the provided JSON Path must +match the regex. + + +byDate(): The value taken from the producer’s response via the provided JSON Path must match +the regex for an ISO Date value. + + +byTimestamp(): The value taken from the producer’s response via the provided JSON Path must +match the regex for an ISO DateTime value. + + +byTime(): The value taken from the producer’s response via the provided JSON Path must match +the regex for an ISO Time value. + + +byType(): The value taken from the producer’s response via the provided JSON Path needs to be +of the same type as the type defined in the body of the response in the contract. +byType can take a closure, in which you can set minOccurrence and maxOccurrence. For the request side, you should use the closure to assert size of the collection. +That way, you can assert the size of the flattened collection. To check the size of an +unflattened collection, use a custom method with the byCommand(…​) testMatcher. + + +byCommand(…​): The value taken from the producer’s response via the provided JSON Path is +passed as an input to the custom method that you provide. For example, +byCommand('foo($it)') results in calling a foo method to which the value matching the +JSON Path gets passed. The type of the object read from the JSON can be one of the +following, depending on the JSON path: + + +String: If you point to a String value. + + +JSONArray: If you point to a List. + + +Map: If you point to a Map. + + +Number: If you point to Integer, Double, or other kind of number. + + +Boolean: If you point to a Boolean. + + + + +byNull(): The value taken from the response via the provided JSON Path must be null + + + + + +YAML +Please read the Groovy section for detailed explanation of +what the types mean + +For YAML the structure of a matcher looks like this +- path: $.foo + type: by_regex + value: bar + regexType: as_string +Or if you want to use one of the predefined regular expressions +[only_alpha_unicode, number, any_boolean, ip_address, hostname, +email, url, uuid, iso_date, iso_date_time, iso_time, iso_8601_with_offset, non_empty, non_blank]: +- path: $.foo + type: by_regex + predefined: only_alpha_unicode +Below you can find the allowed list of `type`s. + + +For stubMatchers: + + +by_equality + + +by_regex + + +by_date + + +by_timestamp + + +by_time + + +by_type + + +there are 2 additional fields accepted: minOccurrence and maxOccurrence. + + + + + + +For testMatchers: + + +by_equality + + +by_regex + + +by_date + + +by_timestamp + + +by_time + + +by_type + + +there are 2 additional fields accepted: minOccurrence and maxOccurrence. + + + + +by_command + + +by_null + + + + +You can also define which type the regular expression corresponds to via the regexType field. Below you can find the allowed list of regular expression types: + + +as_integer + + +as_double + + +as_float, + + +as_long + + +as_short + + +as_boolean + + +as_string + + +Consider the following example: + +Groovy DSL + +Contract contractDsl = Contract.make { + request { + method 'GET' + urlPath '/get' + body([ + duck : 123, + alpha : 'abc', + number : 123, + aBoolean : true, + date : '2017-01-01', + dateTime : '2017-01-01T01:23:45', + time : '01:02:34', + valueWithoutAMatcher: 'foo', + valueWithTypeMatch : 'string', + key : [ + 'complex.key': 'foo' + ] + ]) + bodyMatchers { + jsonPath('$.duck', byRegex("[0-9]{3}").asInteger()) + jsonPath('$.duck', byEquality()) + jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString()) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number()).asInteger()) + jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType()) + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + jsonPath("\$.['key'].['complex.key']", byEquality()) + } + headers { + contentType(applicationJson()) + } + } + response { + status OK() + body([ + duck : 123, + alpha : 'abc', + number : 123, + positiveInteger : 1234567890, + negativeInteger : -1234567890, + positiveDecimalNumber: 123.4567890, + negativeDecimalNumber: -123.4567890, + aBoolean : true, + date : '2017-01-01', + dateTime : '2017-01-01T01:23:45', + time : "01:02:34", + valueWithoutAMatcher : 'foo', + valueWithTypeMatch : 'string', + valueWithMin : [ + 1, 2, 3 + ], + valueWithMax : [ + 1, 2, 3 + ], + valueWithMinMax : [ + 1, 2, 3 + ], + valueWithMinEmpty : [], + valueWithMaxEmpty : [], + key : [ + 'complex.key': 'foo' + ], + nullValue : null + ]) + bodyMatchers { + // asserts the jsonpath value against manual regex + jsonPath('$.duck', byRegex("[0-9]{3}").asInteger()) + // asserts the jsonpath value against the provided value + jsonPath('$.duck', byEquality()) + // asserts the jsonpath value against some default regex + jsonPath('$.alpha', byRegex(onlyAlphaUnicode()).asString()) + jsonPath('$.alpha', byEquality()) + jsonPath('$.number', byRegex(number()).asInteger()) + jsonPath('$.positiveInteger', byRegex(anInteger()).asInteger()) + jsonPath('$.negativeInteger', byRegex(anInteger()).asInteger()) + jsonPath('$.positiveDecimalNumber', byRegex(aDouble()).asDouble()) + jsonPath('$.negativeDecimalNumber', byRegex(aDouble()).asDouble()) + jsonPath('$.aBoolean', byRegex(anyBoolean()).asBooleanType()) + // asserts vs inbuilt time related regex + jsonPath('$.date', byDate()) + jsonPath('$.dateTime', byTimestamp()) + jsonPath('$.time', byTime()) + // asserts that the resulting type is the same as in response body + jsonPath('$.valueWithTypeMatch', byType()) + jsonPath('$.valueWithMin', byType { + // results in verification of size of array (min 1) + minOccurrence(1) + }) + jsonPath('$.valueWithMax', byType { + // results in verification of size of array (max 3) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinMax', byType { + // results in verification of size of array (min 1 & max 3) + minOccurrence(1) + maxOccurrence(3) + }) + jsonPath('$.valueWithMinEmpty', byType { + // results in verification of size of array (min 0) + minOccurrence(0) + }) + jsonPath('$.valueWithMaxEmpty', byType { + // results in verification of size of array (max 0) + maxOccurrence(0) + }) + // will execute a method `assertThatValueIsANumber` + jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)')) + jsonPath("\$.['key'].['complex.key']", byEquality()) + jsonPath('$.nullValue', byNull()) + } + headers { + contentType(applicationJson()) + header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}')))) + } + } +} + + + +YAML + +request: + method: GET + urlPath: /get/1 + headers: + Content-Type: application/json + cookies: + foo: 2 + bar: 3 + queryParameters: + limit: 10 + offset: 20 + filter: 'email' + sort: name + search: 55 + age: 99 + name: John.Doe + email: 'bob@email.com' + body: + duck: 123 + alpha: "abc" + number: 123 + aBoolean: true + date: "2017-01-01" + dateTime: "2017-01-01T01:23:45" + time: "01:02:34" + valueWithoutAMatcher: "foo" + valueWithTypeMatch: "string" + key: + "complex.key": 'foo' + nullValue: null + valueWithMin: + - 1 + - 2 + - 3 + valueWithMax: + - 1 + - 2 + - 3 + valueWithMinMax: + - 1 + - 2 + - 3 + valueWithMinEmpty: [] + valueWithMaxEmpty: [] + matchers: + url: + regex: /get/[0-9] + # predefined: + # execute a method + #command: 'equals($it)' + queryParameters: + - key: limit + type: equal_to + value: 20 + - key: offset + type: containing + value: 20 + - key: sort + type: equal_to + value: name + - key: search + type: not_matching + value: '^[0-9]{2}$' + - key: age + type: not_matching + value: '^\\w*$' + - key: name + type: matching + value: 'John.*' + - key: hello + type: absent + cookies: + - key: foo + regex: '[0-9]' + - key: bar + command: 'equals($it)' + headers: + - key: Content-Type + regex: "application/json.*" + body: + - path: $.duck + type: by_regex + value: "[0-9]{3}" + - path: $.duck + type: by_equality + - path: $.alpha + type: by_regex + predefined: only_alpha_unicode + - path: $.alpha + type: by_equality + - path: $.number + type: by_regex + predefined: number + - path: $.aBoolean + type: by_regex + predefined: any_boolean + - path: $.date + type: by_date + - path: $.dateTime + type: by_timestamp + - path: $.time + type: by_time + - path: "$.['key'].['complex.key']" + type: by_equality + - path: $.nullvalue + type: by_null + - path: $.valueWithMin + type: by_type + minOccurrence: 1 + - path: $.valueWithMax + type: by_type + maxOccurrence: 3 + - path: $.valueWithMinMax + type: by_type + minOccurrence: 1 + maxOccurrence: 3 +response: + status: 200 + cookies: + foo: 1 + bar: 2 + body: + duck: 123 + alpha: "abc" + number: 123 + aBoolean: true + date: "2017-01-01" + dateTime: "2017-01-01T01:23:45" + time: "01:02:34" + valueWithoutAMatcher: "foo" + valueWithTypeMatch: "string" + valueWithMin: + - 1 + - 2 + - 3 + valueWithMax: + - 1 + - 2 + - 3 + valueWithMinMax: + - 1 + - 2 + - 3 + valueWithMinEmpty: [] + valueWithMaxEmpty: [] + key: + 'complex.key': 'foo' + nulValue: null + matchers: + headers: + - key: Content-Type + regex: "application/json.*" + cookies: + - key: foo + regex: '[0-9]' + - key: bar + command: 'equals($it)' + body: + - path: $.duck + type: by_regex + value: "[0-9]{3}" + - path: $.duck + type: by_equality + - path: $.alpha + type: by_regex + predefined: only_alpha_unicode + - path: $.alpha + type: by_equality + - path: $.number + type: by_regex + predefined: number + - path: $.aBoolean + type: by_regex + predefined: any_boolean + - path: $.date + type: by_date + - path: $.dateTime + type: by_timestamp + - path: $.time + type: by_time + - path: $.valueWithTypeMatch + type: by_type + - path: $.valueWithMin + type: by_type + minOccurrence: 1 + - path: $.valueWithMax + type: by_type + maxOccurrence: 3 + - path: $.valueWithMinMax + type: by_type + minOccurrence: 1 + maxOccurrence: 3 + - path: $.valueWithMinEmpty + type: by_type + minOccurrence: 0 + - path: $.valueWithMaxEmpty + type: by_type + maxOccurrence: 0 + - path: $.duck + type: by_command + value: assertThatValueIsANumber($it) + - path: $.nullValue + type: by_null + value: null + headers: + Content-Type: application/json + + +In the preceding example, you can see the dynamic portions of the contract in the +matchers sections. For the request part, you can see that, for all fields but +valueWithoutAMatcher, the values of the regular expressions that the stub should +contain are explicitly set. For the valueWithoutAMatcher, the verification takes place +in the same way as without the use of matchers. In that case, the test performs an +equality check. +For the response side in the bodyMatchers section, we define the dynamic parts in a +similar manner. The only difference is that the byType matchers are also present. The +verifier engine checks four fields to verify whether the response from the test +has a value for which the JSON path matches the given field, is of the same type as the one +defined in the response body, and passes the following check (based on the method being called): + + +For $.valueWithTypeMatch, the engine checks whether the type is the same. + + +For $.valueWithMin, the engine check the type and asserts whether the size is greater +than or equal to the minimum occurrence. + + +For $.valueWithMax, the engine checks the type and asserts whether the size is +smaller than or equal to the maximum occurrence. + + +For $.valueWithMinMax, the engine checks the type and asserts whether the size is +between the min and maximum occurrence. + + +The resulting test would resemble the following example (note that an and section +separates the autogenerated assertions and the assertion from matchers): +// given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/json") + .body("{\"duck\":123,\"alpha\":\"abc\",\"number\":123,\"aBoolean\":true,\"date\":\"2017-01-01\",\"dateTime\":\"2017-01-01T01:23:45\",\"time\":\"01:02:34\",\"valueWithoutAMatcher\":\"foo\",\"valueWithTypeMatch\":\"string\",\"key\":{\"complex.key\":\"foo\"}}"); + +// when: + ResponseOptions response = given().spec(request) + .get("/get"); + +// then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/json.*"); +// and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("['valueWithoutAMatcher']").isEqualTo("foo"); +// and: + assertThat(parsedJson.read("$.duck", String.class)).matches("[0-9]{3}"); + assertThat(parsedJson.read("$.duck", Integer.class)).isEqualTo(123); + assertThat(parsedJson.read("$.alpha", String.class)).matches("[\\p{L}]*"); + assertThat(parsedJson.read("$.alpha", String.class)).isEqualTo("abc"); + assertThat(parsedJson.read("$.number", String.class)).matches("-?(\\d*\\.\\d+|\\d+)"); + assertThat(parsedJson.read("$.aBoolean", String.class)).matches("(true|false)"); + assertThat(parsedJson.read("$.date", String.class)).matches("(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"); + assertThat(parsedJson.read("$.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); + assertThat(parsedJson.read("$.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"); + assertThat((Object) parsedJson.read("$.valueWithTypeMatch")).isInstanceOf(java.lang.String.class); + assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class); + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMin", java.util.Collection.class)).as("$.valueWithMin").hasSizeGreaterThanOrEqualTo(1); + assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class); + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMax", java.util.Collection.class)).as("$.valueWithMax").hasSizeLessThanOrEqualTo(3); + assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class); + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinMax", java.util.Collection.class)).as("$.valueWithMinMax").hasSizeBetween(1, 3); + assertThat((Object) parsedJson.read("$.valueWithMinEmpty")).isInstanceOf(java.util.List.class); + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMinEmpty", java.util.Collection.class)).as("$.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0); + assertThat((Object) parsedJson.read("$.valueWithMaxEmpty")).isInstanceOf(java.util.List.class); + assertThat((java.lang.Iterable) parsedJson.read("$.valueWithMaxEmpty", java.util.Collection.class)).as("$.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0); + assertThatValueIsANumber(parsedJson.read("$.duck")); + assertThat(parsedJson.read("$.['key'].['complex.key']", String.class)).isEqualTo("foo"); + +Notice that, for the byCommand method, the example calls the +assertThatValueIsANumber. This method must be defined in the test base class or be +statically imported to your tests. Notice that the byCommand call was converted to +assertThatValueIsANumber(parsedJson.read("$.duck"));. That means that the engine took +the method name and passed the proper JSON path as a parameter to it. + +The resulting WireMock stub is in the following example: + ''' +{ + "request" : { + "urlPath" : "/get", + "method" : "POST", + "headers" : { + "Content-Type" : { + "matches" : "application/json.*" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$.['list'].['some'].['nested'][?(@.['anothervalue'] == 4)]" + }, { + "matchesJsonPath" : "$[?(@.['valueWithoutAMatcher'] == 'foo')]" + }, { + "matchesJsonPath" : "$[?(@.['valueWithTypeMatch'] == 'string')]" + }, { + "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['json'] == 'with value')]" + }, { + "matchesJsonPath" : "$.['list'].['someother'].['nested'][?(@.['anothervalue'] == 4)]" + }, { + "matchesJsonPath" : "$[?(@.duck =~ /([0-9]{3})/)]" + }, { + "matchesJsonPath" : "$[?(@.duck == 123)]" + }, { + "matchesJsonPath" : "$[?(@.alpha =~ /([\\\\p{L}]*)/)]" + }, { + "matchesJsonPath" : "$[?(@.alpha == 'abc')]" + }, { + "matchesJsonPath" : "$[?(@.number =~ /(-?(\\\\d*\\\\.\\\\d+|\\\\d+))/)]" + }, { + "matchesJsonPath" : "$[?(@.aBoolean =~ /((true|false))/)]" + }, { + "matchesJsonPath" : "$[?(@.date =~ /((\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))/)]" + }, { + "matchesJsonPath" : "$[?(@.dateTime =~ /(([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]" + }, { + "matchesJsonPath" : "$[?(@.time =~ /((2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9]))/)]" + }, { + "matchesJsonPath" : "$.list.some.nested[?(@.json =~ /(.*)/)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithMin.size() >= 1)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithMax.size() <= 3)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithMinMax.size() >= 1 && @.valueWithMinMax.size() <= 3)]" + }, { + "matchesJsonPath" : "$[?(@.valueWithOccurrence.size() >= 4 && @.valueWithOccurrence.size() <= 4)]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\\"duck\\":123,\\"alpha\\":\\"abc\\",\\"number\\":123,\\"aBoolean\\":true,\\"date\\":\\"2017-01-01\\",\\"dateTime\\":\\"2017-01-01T01:23:45\\",\\"time\\":\\"01:02:34\\",\\"valueWithoutAMatcher\\":\\"foo\\",\\"valueWithTypeMatch\\":\\"string\\",\\"valueWithMin\\":[1,2,3],\\"valueWithMax\\":[1,2,3],\\"valueWithMinMax\\":[1,2,3],\\"valueWithOccurrence\\":[1,2,3,4]}", + "headers" : { + "Content-Type" : "application/json" + }, + "transformers" : [ "response-template" ] + } +} +''' + +If you use a matcher, then the part of the request and response that the +matcher addresses with the JSON Path gets removed from the assertion. In the case of +verifying a collection, you must create matchers for all the elements of the +collection. + +Consider the following example: +Contract.make { + request { + method 'GET' + url("/foo") + } + response { + status OK() + body(events: [[ + operation : 'EXPORT', + eventId : '16f1ed75-0bcc-4f0d-a04d-3121798faf99', + status : 'OK' + ], [ + operation : 'INPUT_PROCESSING', + eventId : '3bb4ac82-6652-462f-b6d1-75e424a0024a', + status : 'OK' + ] + ] + ) + bodyMatchers { + jsonPath('$.events[0].operation', byRegex('.+')) + jsonPath('$.events[0].eventId', byRegex('^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$')) + jsonPath('$.events[0].status', byRegex('.+')) + } + } +} +The preceding code leads to creating the following test (the code block shows only the assertion section): +and: + DocumentContext parsedJson = JsonPath.parse(response.body.asString()) + assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("16f1ed75-0bcc-4f0d-a04d-3121798faf99") + assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("EXPORT") + assertThatJson(parsedJson).array("['events']").contains("['operation']").isEqualTo("INPUT_PROCESSING") + assertThatJson(parsedJson).array("['events']").contains("['eventId']").isEqualTo("3bb4ac82-6652-462f-b6d1-75e424a0024a") + assertThatJson(parsedJson).array("['events']").contains("['status']").isEqualTo("OK") +and: + assertThat(parsedJson.read("\$.events[0].operation", String.class)).matches(".+") + assertThat(parsedJson.read("\$.events[0].eventId", String.class)).matches("^([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\$") + assertThat(parsedJson.read("\$.events[0].status", String.class)).matches(".+") +As you can see, the assertion is malformed. Only the first element of the array got +asserted. In order to fix this, you should apply the assertion to the whole $.events +collection and assert it with the byCommand(…​) method. +
    +
    +
    +JAX-RS Support +The Spring Cloud Contract Verifier supports the JAX-RS 2 Client API. The base class needs +to define protected WebTarget webTarget and server initialization. The only option for +testing JAX-RS API is to start a web server. Also, a request with a body needs to have a +content type set. Otherwise, the default of application/octet-stream gets used. +In order to use JAX-RS mode, use the following settings: +testMode == 'JAXRSCLIENT' +The following example shows a generated test API: + ''' + // when: + Response response = webTarget + .path("/users") + .queryParam("limit", "10") + .queryParam("offset", "20") + .queryParam("filter", "email") + .queryParam("sort", "name") + .queryParam("search", "55") + .queryParam("age", "99") + .queryParam("name", "Denis.Stepanov") + .queryParam("email", "bob@email.com") + .request() + .method("GET"); + + String responseAsString = response.readEntity(String.class); + + // then: + assertThat(response.getStatus()).isEqualTo(200); + // and: + DocumentContext parsedJson = JsonPath.parse(responseAsString); + assertThatJson(parsedJson).field("['property1']").isEqualTo("a"); +''' +
    +
    +Async Support +If you’re using asynchronous communication on the server side (your controllers are +returning Callable, DeferredResult, and so on), then, inside your contract, you must +provide an async() method in the response section. The following code shows an example: + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + method GET() + url '/get' + } + response { + status OK() + body 'Passed' + async() + } +} + + + +YAML + +response: + async: true + + +You can also use the fixedDelayMilliseconds method / property to add delay to your stubs. + +Groovy DSL + +org.springframework.cloud.contract.spec.Contract.make { + request { + method GET() + url '/get' + } + response { + status 200 + body 'Passed' + fixedDelayMilliseconds 1000 + } +} + + + +YAML + +response: + fixedDelayMilliseconds: 1000 + + +
    +
    +Working with Context Paths +Spring Cloud Contract supports context paths. + +The only change needed to fully support context paths is the switch on the +PRODUCER side. Also, the autogenerated tests must use EXPLICIT mode. The consumer +side remains untouched. In order for the generated test to pass, you must use EXPLICIT +mode. + + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <testMode>EXPLICIT</testMode> + </configuration> +</plugin> + + + +Gradle + +contracts { + testMode = 'EXPLICIT' +} + + +That way, you generate a test that DOES NOT use MockMvc. It means that you generate +real requests and you need to setup your generated test’s base class to work on a real +socket. +Consider the following contract: +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'GET' + url '/my-context-path/url' + } + response { + status OK() + } +} +The following example shows how to set up a base class and Rest Assured: +import io.restassured.RestAssured; +import org.junit.Before; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = ContextPathTestingBaseClass.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ContextPathTestingBaseClass { + + @LocalServerPort int port; + + @Before + public void setup() { + RestAssured.baseURI = "http://localhost"; + RestAssured.port = this.port; + } +} +If you do it this way: + + +All of your requests in the autogenerated tests are sent to the real endpoint with your +context path included (for example, /my-context-path/url). + + +Your contracts reflect that you have a context path. Your generated stubs also have +that information (for example, in the stubs, you have to call /my-context-path/url). + + +
    +
    +Working with WebFlux +Spring Cloud Contract offers two ways of working with WebFlux. +
    +WebFlux with WebTestClient +One of them is via the WebTestClient mode. + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <testMode>WEBTESTCLIENT</testMode> + </configuration> +</plugin> + + + +Gradle + +contracts { + testMode = 'WEBTESTCLIENT' +} + + +The following example shows how to set up a WebTestClient base class and RestAssured +for WebFlux: +import io.restassured.module.webtestclient.RestAssuredWebTestClient; +import org.junit.Before; + +public abstract class BeerRestBase { + + @Before + public void setup() { + RestAssuredWebTestClient.standaloneSetup( + new ProducerController(personToCheck -> personToCheck.age >= 20)); + } +} +} +
    +
    +WebFlux with Explicit mode +Another way is with the EXPLICIT mode in your generated tests +to work with WebFlux. + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <testMode>EXPLICIT</testMode> + </configuration> +</plugin> + + + +Gradle + +contracts { + testMode = 'EXPLICIT' +} + + +The following example shows how to set up a base class and Rest Assured for Web Flux: +@RunWith(SpringRunner.class) +@SpringBootTest(classes = BeerRestBase.Config.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "server.port=0") +public abstract class BeerRestBase { + + // your tests go here + + // in this config class you define all controllers and mocked services +@Configuration +@EnableAutoConfiguration +static class Config { + + @Bean + PersonCheckingService personCheckingService() { + return personToCheck -> personToCheck.age >= 20; + } + + @Bean + ProducerController producerController() { + return new ProducerController(personCheckingService()); + } +} + +} +
    +
    +
    +XML Support for REST +For REST contracts, we also support XML request and response body. +The XML body has to be passed within the body element +as a String or GString. Also body matchers can be provided for +both request and response. In place of the jsonPath(…​) method, the org.springframework.cloud.contract.spec.internal.BodyMatchers.xPath +method should be used, with the desired xPath provided as the first argument +and the appropriate MatchingType as second. All the body matchers apart from byType() are supported. +Here is an example of a Groovy DSL contract with XML response body: + Contract.make { + request { + method GET() + urlPath '/get' + headers { + contentType(applicationXml()) + } + } + response { + status(OK()) + headers { + contentType(applicationXml()) + } + body """ +<test> +<duck type='xtype'>123</duck> +<alpha>abc</alpha> +<list> +<elem>abc</elem> +<elem>def</elem> +<elem>ghi</elem> +</list> +<number>123</number> +<aBoolean>true</aBoolean> +<date>2017-01-01</date> +<dateTime>2017-01-01T01:23:45</dateTime> +<time>01:02:34</time> +<valueWithoutAMatcher>foo</valueWithoutAMatcher> +<key><complex>foo</complex></key> +</test>""" + bodyMatchers { + xPath('/test/duck/text()', byRegex("[0-9]{3}")) + xPath('/test/duck/text()', byCommand('test($it)')) + xPath('/test/duck/xxx', byNull()) + xPath('/test/duck/text()', byEquality()) + xPath('/test/alpha/text()', byRegex(onlyAlphaUnicode())) + xPath('/test/alpha/text()', byEquality()) + xPath('/test/number/text()', byRegex(number())) + xPath('/test/date/text()', byDate()) + xPath('/test/dateTime/text()', byTimestamp()) + xPath('/test/time/text()', byTime()) + xPath('/test/*/complex/text()', byEquality()) + xPath('/test/duck/@type', byEquality()) + } + } + } +And below is an example of a YAML contract with XML request and response bodies: +include::{verifier_core_path}/src/test/resources/yml/contract_rest_xml.yml +Here is an example of an automatically generated test for XML response body: +@Test +public void validate_xmlMatches() throws Exception { + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/xml"); + + // when: + ResponseOptions response = given().spec(request).get("/get"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + // and: + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance() + .newDocumentBuilder(); + Document parsedXml = documentBuilder.parse(new InputSource( + new StringReader(response.getBody().asString()))); + // and: + assertThat(valueFromXPath(parsedXml, "/test/list/elem/text()")).isEqualTo("abc"); + assertThat(valueFromXPath(parsedXml,"/test/list/elem[2]/text()")).isEqualTo("def"); + assertThat(valueFromXPath(parsedXml, "/test/duck/text()")).matches("[0-9]{3}"); + assertThat(nodeFromXPath(parsedXml, "/test/duck/xxx")).isNull(); + assertThat(valueFromXPath(parsedXml, "/test/alpha/text()")).matches("[\\p{L}]*"); + assertThat(valueFromXPath(parsedXml, "/test/*/complex/text()")).isEqualTo("foo"); + assertThat(valueFromXPath(parsedXml, "/test/duck/@type")).isEqualTo("xtype"); + } +
    +
    +Messaging Top-Level Elements +The DSL for messaging looks a little bit different than the one that focuses on HTTP. The +following sections explain the differences: + + + + + + + + + + + + + + +
    +Output Triggered by a Method +The output message can be triggered by calling a method (such as a Scheduler when a was +started and a message was sent), as shown in the following example: + +Groovy DSL + +def dsl = Contract.make { + // Human readable description + description 'Some description' + // Label by means of which the output message can be triggered + label 'some_label' + // input to the contract + input { + // the contract will be triggered by a method + triggeredBy('bookReturnedTriggered()') + } + // output message of the contract + outputMessage { + // destination to which the output message will be sent + sentTo('output') + // the body of the output message + body('''{ "bookName" : "foo" }''') + // the headers of the output message + headers { + header('BOOK-NAME', 'foo') + } + } +} + + + +YAML + +# Human readable description +description: Some description +# Label by means of which the output message can be triggered +label: some_label +input: + # the contract will be triggered by a method + triggeredBy: bookReturnedTriggered() +# output message of the contract +outputMessage: + # destination to which the output message will be sent + sentTo: output + # the body of the output message + body: + bookName: foo + # the headers of the output message + headers: + BOOK-NAME: foo + + +In the previous example case, the output message is sent to output if a method called +bookReturnedTriggered is executed. On the message publisher’s side, we generate a +test that calls that method to trigger the message. On the consumer side, you can use +the some_label to trigger the message. +
    +
    +Output Triggered by a Message +The output message can be triggered by receiving a message, as shown in the following +example: + +Groovy DSL + +def dsl = Contract.make { + description 'Some Description' + label 'some_label' + // input is a message + input { + // the message was received from this destination + messageFrom('input') + // has the following body + messageBody([ + bookName: 'foo' + ]) + // and the following headers + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo('output') + body([ + bookName: 'foo' + ]) + headers { + header('BOOK-NAME', 'foo') + } + } +} + + + +YAML + +# Human readable description +description: Some description +# Label by means of which the output message can be triggered +label: some_label +# input is a message +input: + messageFrom: input + # has the following body + messageBody: + bookName: 'foo' + # and the following headers + messageHeaders: + sample: 'header' +# output message of the contract +outputMessage: + # destination to which the output message will be sent + sentTo: output + # the body of the output message + body: + bookName: foo + # the headers of the output message + headers: + BOOK-NAME: foo + + +In the preceding example, the output message is sent to output if a proper message is +received on the input destination. On the message publisher’s side, the engine +generates a test that sends the input message to the defined destination. On the +consumer side, you can either send a message to the input destination or use a label +(some_label in the example) to trigger the message. +
    +
    +Consumer/Producer + +This section is valid only for Groovy DSL. + +In HTTP, you have a notion of client/stub and `server/test notation. You can also +use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also +provides the consumer and producer methods, as presented in the following example +(note that you can use either $ or value methods to provide consumer and producer +parts): + Contract.make { + label 'some_label' + input { + messageFrom value(consumer('jms:output'), producer('jms:input')) + messageBody([ + bookName: 'foo' + ]) + messageHeaders { + header('sample', 'header') + } + } + outputMessage { + sentTo $(consumer('jms:input'), producer('jms:output')) + body([ + bookName: 'foo' + ]) + } + } +
    +
    +Common +In the input or outputMessage section you can call assertThat with the name +of a method (e.g. assertThatMessageIsOnTheQueue()) that you have defined in the +base class or in a static import. Spring Cloud Contract will execute that method +in the generated test. +
    +
    +
    +Multiple Contracts in One File +You can define multiple contracts in one file. Such a contract might resemble the +following example: + +Groovy DSL + +import org.springframework.cloud.contract.spec.Contract + +[ + Contract.make { + name("should post a user") + request { + method 'POST' + url('/users/1') + } + response { + status OK() + } + }, + Contract.make { + request { + method 'POST' + url('/users/2') + } + response { + status OK() + } + } +] + + + +YAML + +--- +name: should post a user +request: + method: POST + url: /users/1 +response: + status: 200 +--- +request: + method: POST + url: /users/2 +response: + status: 200 +--- +request: + method: POST + url: /users/3 +response: + status: 200 + + +In the preceding example, one contract has the name field and the other does not. This +leads to generation of two tests that look more or less like this: +package org.springframework.cloud.contract.verifier.tests.com.hello; + +import com.example.TestBase; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification; +import com.jayway.restassured.response.ResponseOptions; +import org.junit.Test; + +import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*; +import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public class V1Test extends TestBase { + + @Test + public void validate_should_post_a_user() throws Exception { + // given: + MockMvcRequestSpecification request = given(); + + // when: + ResponseOptions response = given().spec(request) + .post("/users/1"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + } + + @Test + public void validate_withList_1() throws Exception { + // given: + MockMvcRequestSpecification request = given(); + + // when: + ResponseOptions response = given().spec(request) + .post("/users/2"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + } + +} +Notice that, for the contract that has the name field, the generated test method is named +validate_should_post_a_user. For the one that does not have the name, it is called +validate_withList_1. It corresponds to the name of the file WithList.groovy and the +index of the contract in the list. +The generated stubs is shown in the following example: +should post a user.json +1_WithList.json +As you can see, the first file got the name parameter from the contract. The second +got the name of the contract file (WithList.groovy) prefixed with the index (in this +case, the contract had an index of 1 in the list of contracts in the file). + +As you can see, it is much better if you name your contracts because doing so makes +your tests far more meaningful. + +
    +
    +Generating Spring REST Docs snippets from the contracts +When you want to include the requests and responses of your API using Spring REST Docs, +you only need to make some minor changes to your setup if you are using MockMvc and RestAssuredMockMvc. +Simply include the following dependencies if you haven’t already. + +Maven + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-contract-verifier</artifactId> + <scope>test</scope> +</dependency> +<dependency> + <groupId>org.springframework.restdocs</groupId> + <artifactId>spring-restdocs-mockmvc</artifactId> + <optional>true</optional> +</dependency> + + + +Gradle + +testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' +testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' + + +Next you need to make some changes to your base class like the following example. +package com.example.fraud; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class) +public abstract class FraudBaseWithWebAppSetup { + + private static final String OUTPUT = "target/generated-snippets"; + + @Rule + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT); + + @Rule + public TestName testName = new TestName(); + + @Autowired + private WebApplicationContext context; + + @Before + public void setup() { + RestAssuredMockMvc.mockMvc(MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(document( + getClass().getSimpleName() + "_" + testName.getMethodName())) + .build()); + } + + protected void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } + +} +In case you are using the standalone setup, you can set up RestAssuredMockMvc like this: +package com.example.fraud; + +import io.restassured.module.mockmvc.RestAssuredMockMvc; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestName; + +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +public abstract class FraudBaseWithStandaloneSetup { + + private static final String OUTPUT = "target/generated-snippets"; + + @Rule + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(OUTPUT); + + @Rule + public TestName testName = new TestName(); + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(MockMvcBuilders + .standaloneSetup(new FraudDetectionController()) + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(document( + getClass().getSimpleName() + "_" + testName.getMethodName()))); + } + +} + +You don’t need to specify the output directory for the generated snippets since version 1.2.0.RELEASE of Spring REST Docs. + +
    +
    + +Customization + +This section is valid only for Groovy DSL + +You can customize the Spring Cloud Contract Verifier by extending the DSL, as shown in +the remainder of this section. +
    +Extending the DSL +You can provide your own functions to the DSL. The key requirement for this feature is to +maintain the static compatibility. Later in this document, you can see examples of: + + +Creating a JAR with reusable classes. + + +Referencing of these classes in the DSLs. + + +You can find the full example +here. +
    +Common JAR +The following examples show three classes that can be reused in the DSLs. +PatternUtils contains functions used by both the consumer and the producer. +package com.example; + +import java.util.regex.Pattern; + +/** + * If you want to use {@link Pattern} directly in your tests + * then you can create a class resembling this one. It can + * contain all the {@link Pattern} you want to use in the DSL. + * + * <pre> + * {@code + * request { + * body( + * [ age: $(c(PatternUtils.oldEnough()))] + * ) + * } + * </pre> + * + * Notice that we're using both {@code $()} for dynamic values + * and {@code c()} for the consumer side. + * + * @author Marcin Grzejszczak + */ +//tag::impl[] +public class PatternUtils { + + public static String tooYoung() { + //remove::start[] + return "[0-1][0-9]"; + //remove::end[return] + } + + public static Pattern oldEnough() { + //remove::start[] + return Pattern.compile("[2-9][0-9]"); + //remove::end[return] + } + + /** + * Makes little sense but it's just an example ;) + */ + public static Pattern ok() { + //remove::start[] + return Pattern.compile("OK"); + //remove::end[return] + } +} +//end::impl[] +ConsumerUtils contains functions used by the consumer. +package com.example; + +import org.springframework.cloud.contract.spec.internal.ClientDslProperty; + +/** + * DSL Properties passed to the DSL from the consumer's perspective. + * That means that on the input side {@code Request} for HTTP + * or {@code Input} for messaging you can have a regular expression. + * On the {@code Response} for HTTP or {@code Output} for messaging + * you have to have a concrete value. + * + * @author Marcin Grzejszczak + */ +//tag::impl[] +public class ConsumerUtils { + /** + * Consumer side property. By using the {@link ClientDslProperty} + * you can omit most of boilerplate code from the perspective + * of dynamic values. Example + * + * <pre> + * {@code + * request { + * body( + * [ age: $(ConsumerUtils.oldEnough())] + * ) + * } + * </pre> + * + * That way it's in the implementation that we decide what value we will pass to the consumer + * and which one to the producer. + * + * @author Marcin Grzejszczak + */ + public static ClientDslProperty oldEnough() { + //remove::start[] + // this example is not the best one and + // theoretically you could just pass the regex instead of `ServerDslProperty` but + // it's just to show some new tricks :) + return new ClientDslProperty(PatternUtils.oldEnough(), 40); + //remove::end[return] + } + +} +//end::impl[] +ProducerUtils contains functions used by the producer. +package com.example; + +import org.springframework.cloud.contract.spec.internal.ServerDslProperty; + +/** + * DSL Properties passed to the DSL from the producer's perspective. + * That means that on the input side {@code Request} for HTTP + * or {@code Input} for messaging you have to have a concrete value. + * On the {@code Response} for HTTP or {@code Output} for messaging + * you can have a regular expression. + * + * @author Marcin Grzejszczak + */ +//tag::impl[] +public class ProducerUtils { + + /** + * Producer side property. By using the {@link ProducerUtils} + * you can omit most of boilerplate code from the perspective + * of dynamic values. Example + * + * <pre> + * {@code + * response { + * body( + * [ status: $(ProducerUtils.ok())] + * ) + * } + * </pre> + * + * That way it's in the implementation that we decide what value we will pass to the consumer + * and which one to the producer. + */ + public static ServerDslProperty ok() { + // this example is not the best one and + // theoretically you could just pass the regex instead of `ServerDslProperty` but + // it's just to show some new tricks :) + return new ServerDslProperty( PatternUtils.ok(), "OK"); + } +} +//end::impl[] +
    +
    +Adding the Dependency to the Project +In order for the plugins and IDE to be able to reference the common JAR classes, you need +to pass the dependency to your project. +
    +
    +Test the Dependency in the Project’s Dependencies +First, add the common jar dependency as a test dependency. Because your contracts files +are available on the test resources path, the common jar classes automatically become +visible in your Groovy files. The following examples show how to test the dependency: + +Maven + +<dependency> + <groupId>com.example</groupId> + <artifactId>beer-common</artifactId> + <version>${project.version}</version> + <scope>test</scope> +</dependency> + + + +Gradle + +testCompile("com.example:beer-common:0.0.1.BUILD-SNAPSHOT") + + +
    +
    +Test a Dependency in the Plugin’s Dependencies +Now, you must add the dependency for the plugin to reuse at runtime, as shown in the +following example: + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example</packageWithBaseClasses> + <baseClassMappings> + <baseClassMapping> + <contractPackageRegex>.*intoxication.*</contractPackageRegex> + <baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN> + </baseClassMapping> + </baseClassMappings> + </configuration> + <dependencies> + <dependency> + <groupId>com.example</groupId> + <artifactId>beer-common</artifactId> + <version>${project.version}</version> + <scope>compile</scope> + </dependency> + </dependencies> +</plugin> + + + +Gradle + +classpath "com.example:beer-common:0.0.1.BUILD-SNAPSHOT" + + +
    +
    +Referencing classes in DSLs +You can now reference your classes in your DSL, as shown in the following example: +package contracts.beer.rest + +import com.example.ConsumerUtils +import com.example.ProducerUtils +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + description(""" +Represents a successful scenario of getting a beer + +``` +given: + client is old enough +when: + he applies for a beer +then: + we'll grant him the beer +``` + +""") + request { + method 'POST' + url '/check' + body( + age: $(ConsumerUtils.oldEnough()) + ) + headers { + contentType(applicationJson()) + } + } + response { + status 200 + body(""" + { + "status": "${value(ProducerUtils.ok())}" + } + """) + headers { + contentType(applicationJson()) + } + } +} + +You can set the Spring Cloud Contract plugin up by setting convertToYaml to true. That way you will NOT have to add the dependency with the extended functionality to the consumer side, since the consumer side will be using YAML contracts instead of Groovy ones. + +
    +
    +
    + +Using the Pluggable Architecture +You may encounter cases where you have your contracts have been defined in other formats, +such as YAML, RAML or PACT. In those cases, you still want to benefit from the automatic +generation of tests and stubs. You can add your own implementation for generating both +tests and stubs. Also, you can customize the way tests are generated (for example, you +can generate tests for other languages) and the way stubs are generated (for example, you +can generate stubs for other HTTP server implementations). +
    +Custom Contract Converter +The ContractConverter interface lets you register your own implementation of a contract +structure converter. The following code listing shows the ContractConverter interface: +package org.springframework.cloud.contract.spec + +/** + * Converter to be used to convert FROM {@link File} TO {@link Contract} + * and from {@link Contract} to {@code T} + * + * @param <T > - type to which we want to convert the contract + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +interface ContractConverter<T> extends ContractStorer<T> { + + /** + * Should this file be accepted by the converter. Can use the file extension + * to check if the conversion is possible. + * + * @param file - file to be considered for conversion + * @return - {@code true} if the given implementation can convert the file + */ + boolean isAccepted(File file) + + /** + * Converts the given {@link File} to its {@link Contract} representation + * + * @param file - file to convert + * @return - {@link Contract} representation of the file + */ + Collection<Contract> convertFrom(File file) + + /** + * Converts the given {@link Contract} to a {@link T} representation + * + * @param contract - the parsed contract + * @return - {@link T} the type to which we do the conversion + */ + T convertTo(Collection<Contract> contract) +} +Your implementation must define the condition on which it should start the +conversion. Also, you must define how to perform that conversion in both directions. + +Once you create your implementation, you must create a +/META-INF/spring.factories file in which you provide the fully qualified name of your +implementation. + +The following example shows a typical spring.factories file: +org.springframework.cloud.contract.spec.ContractConverter=\ +org.springframework.cloud.contract.verifier.converter.YamlContractConverter +
    +Pact Converter +Spring Cloud Contract includes support for Pact representation of +contracts up until v4. Instead of using the Groovy DSL, you can use Pact files. In this section, we +present how to add Pact support for your project. Note however that not all functionality is supported. +Starting with v3 you can combine multiple matcher for the same element; +you can use matchers for the body, headers, request and path; and you can use value generators. +Spring Cloud Contract currently only supports multiple matchers that are combined using the AND rule logic. +Next to that the request and path matchers are skipped during the conversion. +When using a date, time or datetime value generator with a given format, +the given format will be skipped and the ISO format will be used. +In order to properly support the Spring Cloud Contract way of doing messaging +with Pact you’ll have to provide some additional meta data entries. Below you can find a list of such entries: + + +to define the destination to which a message gets sent, you have to +set a metaData entry in the Pact file, with key sentTo equal to the destination to which a message is to be sent. E.g. "metaData": { "sentTo": "activemq:output" } + + +
    +
    +Pact Contract +Consider following example of a Pact contract, which is a file under the +src/test/resources/contracts folder. +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "PUT", + "path": "/fraudcheck", + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "clientId": "1234567890", + "loanAmount": 99999 + }, + "generators": { + "body": { + "$.clientId": { + "type": "Regex", + "regex": "[0-9]{10}" + } + } + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/vnd\\.fraud\\.v1\\+json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.clientId": { + "matchers": [ + { + "match": "regex", + "regex": "[0-9]{10}" + } + ], + "combine": "AND" + } + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "fraudCheckStatus": "FRAUD", + "rejectionReason": "Amount too high" + }, + "matchingRules": { + "header": { + "Content-Type": { + "matchers": [ + { + "match": "regex", + "regex": "application/vnd\\.fraud\\.v1\\+json.*" + } + ], + "combine": "AND" + } + }, + "body": { + "$.fraudCheckStatus": { + "matchers": [ + { + "match": "regex", + "regex": "FRAUD" + } + ], + "combine": "AND" + } + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "3.0.0" + }, + "pact-jvm": { + "version": "3.5.13" + } + } +} +The remainder of this section about using Pact refers to the preceding file. +
    +
    +Pact for Producers +On the producer side, you must add two additional dependencies to your plugin +configuration. One is the Spring Cloud Contract Pact support, and the other represents +the current Pact version that you use. + +Maven + +<plugin> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-maven-plugin</artifactId> + <version>${spring-cloud-contract.version}</version> + <extensions>true</extensions> + <configuration> + <packageWithBaseClasses>com.example.fraud</packageWithBaseClasses> + </configuration> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-pact</artifactId> + <version>${spring-cloud-contract.version}</version> + </dependency> + </dependencies> +</plugin> + + + +Gradle + +classpath "org.springframework.cloud:spring-cloud-contract-pact:${findProperty('verifierVersion') ?: verifierVersion}" + + +When you execute the build of your application, a test will be generated. The generated +test might be as follows: +@Test +public void validate_shouldMarkClientAsFraud() throws Exception { + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/vnd.fraud.v1+json") + .body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}"); + + // when: + ResponseOptions response = given().spec(request) + .put("/fraudcheck"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).matches("application/vnd\\.fraud\\.v1\\+json.*"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("['rejectionReason']").isEqualTo("Amount too high"); + // and: + assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD"); +} +The corresponding generated stub might be as follows: +{ + "id" : "996ae5ae-6834-4db6-8fac-358ca187ab62", + "uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62", + "request" : { + "url" : "/fraudcheck", + "method" : "PUT", + "headers" : { + "Content-Type" : { + "matches" : "application/vnd\\.fraud\\.v1\\+json.*" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.['loanAmount'] == 99999)]" + }, { + "matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}", + "headers" : { + "Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "transformers" : [ "response-template" ] + }, +} +
    +
    +Pact for Consumers +On the consumer side, you must add two additional dependencies to your project +dependencies. One is the Spring Cloud Contract Pact support, and the other represents the +current Pact version that you use. + +Maven + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-contract-pact</artifactId> + <scope>test</scope> +</dependency> + + + +Gradle + +testCompile "org.springframework.cloud:spring-cloud-contract-pact" + + +
    +
    +
    +Using the Custom Test Generator +If you want to generate tests for languages other than Java or you are not happy with the +way the verifier builds Java tests, you can register your own implementation. +The SingleTestGenerator interface lets you register your own implementation. The +following code listing shows the SingleTestGenerator interface: +package org.springframework.cloud.contract.verifier.builder + + +import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +/** + * Builds a single test. + * + * @since 1.1.0 + */ +trait SingleTestGenerator { + + /** + * Creates contents of a single test class in which all test scenarios from + * the contract metadata should be placed. + * + * @param properties - properties passed to the plugin + * @param listOfFiles - list of parsed contracts with additional metadata + * @param className - the name of the generated test class + * @param classPackage - the name of the package in which the test class should be stored + * @param includedDirectoryRelativePath - relative path to the included directory + * @return contents of a single test class + * @deprecated use{@link SingleTestGenerator#buildClass(ContractVerifierConfigProperties, Collection, String, GeneratedClassData)} + */ + @Deprecated + abstract String buildClass(ContractVerifierConfigProperties properties, + Collection<ContractMetadata> listOfFiles, String className, String classPackage, String includedDirectoryRelativePath) + + /** + * Creates contents of a single test class in which all test scenarios from + * the contract metadata should be placed. + * + * @param properties - properties passed to the plugin + * @param listOfFiles - list of parsed contracts with additional metadata + * @param generatedClassData - information about the generated class + * @param includedDirectoryRelativePath - relative path to the included directory + * @return contents of a single test class + */ + String buildClass(ContractVerifierConfigProperties properties, + Collection<ContractMetadata> listOfFiles, String includedDirectoryRelativePath, GeneratedClassData generatedClassData) { + String className = generatedClassData.className + String classPackage = generatedClassData.classPackage + String path = includedDirectoryRelativePath + return buildClass(properties, listOfFiles, className, classPackage, path) + } + + /** + * Extension that should be appended to the generated test class. E.g. {@code .java} or {@code .php} + * + * @param properties - properties passed to the plugin + */ + abstract String fileExtension(ContractVerifierConfigProperties properties) + + static class GeneratedClassData { + public final String className + public final String classPackage + public final java.nio.file.Path testClassPath + + GeneratedClassData(String className, String classPackage, + java.nio.file.Path testClassPath) { + this.className = className + this.classPackage = classPackage + this.testClassPath = testClassPath + } + } +} +Again, you must provide a spring.factories file, such as the one shown in the following +example: +org.springframework.cloud.contract.verifier.builder.SingleTestGenerator=/ +com.example.MyGenerator +
    +
    +Using the Custom Stub Generator +If you want to generate stubs for stub servers other than WireMock, you can plug in your +own implementation of the StubGenerator interface. The following code listing shows the +StubGenerator interface: +package org.springframework.cloud.contract.verifier.converter + +import groovy.transform.CompileStatic + +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.file.ContractMetadata + +/** + * Converts contracts into their stub representation. + * + * @since 1.1.0 + */ +@CompileStatic +interface StubGenerator { + + /** + * @return {@code true} if the converter can handle the file to convert it into a stub. + */ + boolean canHandleFileName(String fileName) + + /** + * @return the collection of converted contracts into stubs. One contract can + * result in multiple stubs. + */ + Map<Contract, String> convertContents(String rootName, ContractMetadata content) + + /** + * @return the name of the converted stub file. If you have multiple contracts + * in a single file then a prefix will be added to the generated file. If you + * provide the {@link Contract#name} field then that field will override the + * generated file name. + * + * Example: name of file with 2 contracts is {@code foo.groovy}, it will be + * converted by the implementation to {@code foo.json}. The recursive file + * converter will create two files {@code 0_foo.json} and {@code 1_foo.json} + */ + String generateOutputFileNameForInput(String inputFileName) +} +Again, you must provide a spring.factories file, such as the one shown in the following +example: +# Stub converters +org.springframework.cloud.contract.verifier.converter.StubGenerator=\ +org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter +The default implementation is the WireMock stub generation. + +You can provide multiple stub generator implementations. For example, from a single +DSL, you can produce both WireMock stubs and Pact files. + +
    +
    +Using the Custom Stub Runner +If you decide to use a custom stub generation, you also need a custom way of running +stubs with your different stub provider. +Assume that you use Moco to build your stubs and that +you have written a stub generator and placed your stubs in a JAR file. +In order for Stub Runner to know how to run your stubs, you have to define a custom +HTTP Stub server implementation, which might resemble the following example: +package org.springframework.cloud.contract.stubrunner.provider.moco + +import com.github.dreamhead.moco.bootstrap.arg.HttpArgs +import com.github.dreamhead.moco.runner.JsonRunner +import com.github.dreamhead.moco.runner.RunnerSetting +import groovy.util.logging.Commons + +import org.springframework.cloud.contract.stubrunner.HttpServerStub +import org.springframework.util.SocketUtils + +@Commons +class MocoHttpServerStub implements HttpServerStub { + + private boolean started + private JsonRunner runner + private int port + + @Override + int port() { + if (!isRunning()) { + return -1 + } + return port + } + + @Override + boolean isRunning() { + return started + } + + @Override + HttpServerStub start() { + return start(SocketUtils.findAvailableTcpPort()) + } + + @Override + HttpServerStub start(int port) { + this.port = port + return this + } + + @Override + HttpServerStub stop() { + if (!isRunning()) { + return this + } + this.runner.stop() + return this + } + + @Override + HttpServerStub registerMappings(Collection<File> stubFiles) { + List<RunnerSetting> settings = stubFiles.findAll { it.name.endsWith("json") } + .collect { + log.info("Trying to parse [${it.name}]") + try { + return RunnerSetting.aRunnerSetting().withStream(it.newInputStream()). + build() + } + catch (Exception e) { + log.warn("Exception occurred while trying to parse file [${it.name}]", e) + return null + } + }.findAll { it } + this.runner = JsonRunner.newJsonRunnerWithSetting(settings, + HttpArgs.httpArgs().withPort(this.port).build()) + this.runner.run() + this.started = true + return this + } + + @Override + String registeredMappings() { + return "" + } + + @Override + boolean isAccepted(File file) { + return file.name.endsWith(".json") + } +} +Then, you can register it in your spring.factories file, as shown in the following +example: +org.springframework.cloud.contract.stubrunner.HttpServerStub=\ +org.springframework.cloud.contract.stubrunner.provider.moco.MocoHttpServerStub +Now you can run stubs with Moco. + +If you do not provide any implementation, then the default (WireMock) +implementation is used. If you provide more than one, the first one on the list is used. + +
    +
    +Using the Custom Stub Downloader +You can customize the way your stubs are downloaded by creating an implementation of the +StubDownloaderBuilder interface, as shown in the following example: +package com.example; + +class CustomStubDownloaderBuilder implements StubDownloaderBuilder { + + @Override + public StubDownloader build(final StubRunnerOptions stubRunnerOptions) { + return new StubDownloader() { + @Override + public Map.Entry<StubConfiguration, File> downloadAndUnpackStubJar( + StubConfiguration config) { + File unpackedStubs = retrieveStubs(); + return new AbstractMap.SimpleEntry<>( + new StubConfiguration(config.getGroupId(), config.getArtifactId(), version, + config.getClassifier()), unpackedStubs); + } + + File retrieveStubs() { + // here goes your custom logic to provide a folder where all the stubs reside + } +} +Then you can register it in your spring.factories file, as shown in the following +example: +# Example of a custom Stub Downloader Provider +org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder=\ +com.example.CustomStubDownloaderBuilder +Now you can pick a folder with the source of your stubs. + +If you do not provide any implementation, then the default is used (scan classpath). +If you provide the stubsMode = StubRunnerProperties.StubsMode.LOCAL or +, stubsMode = StubRunnerProperties.StubsMode.REMOTE then the Aether implementation will be used +If you provide more than one, then the first one on the list is used. + +
    +
    +Using the SCM Stub Downloader +Whenever the repositoryRoot starts with a SCM protocol +(currently we support only git://), the stub downloader will try +to clone the repository and use it as a source of contracts +to generate tests or stubs. +Either via environment variables, system properties, properties set +inside the plugin or contracts repository configuration you can +tweak the downloader’s behaviour. Below you can find the list of +properties + +SCM Stub Downloader properties + + + + + + +Type of a property +Name of the property +Description + + +* git.branch (plugin prop)* stubrunner.properties.git.branch (system prop)* STUBRUNNER_PROPERTIES_GIT_BRANCH (env prop) +master +Which branch to checkout + + +* git.username (plugin prop)* stubrunner.properties.git.username (system prop)* STUBRUNNER_PROPERTIES_GIT_USERNAME (env prop) + +Git clone username + + +* git.password (plugin prop)* stubrunner.properties.git.password (system prop)* STUBRUNNER_PROPERTIES_GIT_PASSWORD (env prop) + +Git clone password + + +* git.no-of-attempts (plugin prop)* stubrunner.properties.git.no-of-attempts (system prop)* STUBRUNNER_PROPERTIES_GIT_NO_OF_ATTEMPTS (env prop) +10 +Number of attempts to push the commits to origin + + +* git.wait-between-attempts (Plugin prop)* stubrunner.properties.git.wait-between-attempts (system prop)* STUBRUNNER_PROPERTIES_GIT_WAIT_BETWEEN_ATTEMPTS (env prop) +1000 +Number of millis to wait between attempts to push the commits to origin + + + +
    +
    +
    +Using the Pact Stub Downloader +Whenever the repositoryRoot starts with a Pact protocol +(starts with pact://), the stub downloader will try +to fetch the Pact contract definitions from the Pact Broker. +Whatever is set after pact:// will be parsed as the Pact Broker URL. +Either via environment variables, system properties, properties set +inside the plugin or contracts repository configuration you can +tweak the downloader’s behaviour. Below you can find the list of +properties + +SCM Stub Downloader properties + + + + + + +Name of a property +Default +Description + + +* pactbroker.host (plugin prop)* stubrunner.properties.pactbroker.host (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_HOST (env prop) +Host from URL passed to repositoryRoot +What is the URL of Pact Broker + + +* pactbroker.port (plugin prop)* stubrunner.properties.pactbroker.port (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_PORT (env prop) +Port from URL passed to repositoryRoot +What is the port of Pact Broker + + +* pactbroker.protocol (plugin prop)* stubrunner.properties.pactbroker.protocol (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_PROTOCOL (env prop) +Protocol from URL passed to repositoryRoot +What is the protocol of Pact Broker + + +* pactbroker.tags (plugin prop)* stubrunner.properties.pactbroker.tags (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_TAGS (env prop) +Version of the stub, or latest if version is + +What tags should be used to fetch the stub + + +* pactbroker.auth.scheme (plugin prop)* stubrunner.properties.pactbroker.auth.scheme (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_SCHEME (env prop) +Basic +What kind of authentication should be used to connect to the Pact Broker + + +* pactbroker.auth.username (plugin prop)* stubrunner.properties.pactbroker.auth.username (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_USERNAME (env prop) +The username passed to contractsRepositoryUsername (maven) or contractRepository.username (gradle) +Username used to connect to the Pact Broker + + +* pactbroker.auth.password (plugin prop)* stubrunner.properties.pactbroker.auth.password (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_AUTH_PASSWORD (env prop) +The password passed to contractsRepositoryPassword (maven) or contractRepository.password (gradle) +Password used to connect to the Pact Broker + + +* pactbroker.provider-name-with-group-id (plugin prop)* stubrunner.properties.pactbroker.provider-name-with-group-id (system prop)* STUBRUNNER_PROPERTIES_PACTBROKER_PROVIDER_NAME_WITH_GROUP_ID (env prop) +false +When true, the provider name will be a combination of groupId:artifactId. If false, just artifactId is used + + + +
    +
    +
    + +Spring Cloud Contract WireMock +The Spring Cloud Contract WireMock modules let you use WireMock in a +Spring Boot application. Check out the +samples +for more details. +If you have a Spring Boot application that uses Tomcat as an embedded server (which is +the default with spring-boot-starter-web), you can add +spring-cloud-starter-contract-stub-runner to your classpath and add @AutoConfigureWireMock in +order to be able to use Wiremock in your tests. Wiremock runs as a stub server and you +can register stub behavior using a Java API or via static JSON declarations as part of +your test. The following code shows an example: +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureWireMock(port = 0) +public class WiremockForDocsTests { + + // A service that calls out over HTTP + @Autowired + private Service service; + + @Before + public void setup() { + this.service.setBase("http://localhost:" + + this.environment.getProperty("wiremock.server.port")); + } + + // Using the WireMock APIs in the normal way: + @Test + public void contextLoads() throws Exception { + // Stubbing WireMock + stubFor(get(urlEqualTo("/resource")).willReturn(aResponse() + .withHeader("Content-Type", "text/plain").withBody("Hello World!"))); + // We're asserting if WireMock responded properly + assertThat(this.service.go()).isEqualTo("Hello World!"); + } + +} +To start the stub server on a different port use (for example), +@AutoConfigureWireMock(port=9999). For a random port, use a value of 0. The stub +server port can be bound in the test application context with the "wiremock.server.port" +property. Using @AutoConfigureWireMock adds a bean of type WiremockConfiguration to +your test application context, where it will be cached in between methods and classes +having the same context, the same as for Spring integration tests. Also you can inject a bean of type WireMockServer into your test. +
    +Registering Stubs Automatically +If you use @AutoConfigureWireMock, it registers WireMock JSON stubs from the file +system or classpath (by default, from file:src/test/resources/mappings). You can +customize the locations using the stubs attribute in the annotation, which can be an +Ant-style resource pattern or a directory. In the case of a directory, */.json is +appended. The following code shows an example: +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureWireMock(stubs="classpath:/stubs") +public class WiremockImportApplicationTests { + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + assertThat(this.service.go()).isEqualTo("Hello World!"); + } + +} + +Actually, WireMock always loads mappings from src/test/resources/mappings as +well as the custom locations in the stubs attribute. To change this behavior, you can +also specify a files root as described in the next section of this document. + +If you’re using Spring Cloud Contract’s default stub jars, then your +stubs are stored under /META-INF/group-id/artifact-id/versions/mappings/ folder. If you want to register all stubs from that location, from all embedded JARs, then it’s enough to use the following syntax. +@AutoConfigureWireMock(port = 0, stubs = "classpath*:/META-INF/**/mappings/**/*.json") +
    +
    +Using Files to Specify the Stub Bodies +WireMock can read response bodies from files on the classpath or the file system. In that +case, you can see in the JSON DSL that the response has a bodyFileName instead of a +(literal) body. The files are resolved relative to a root directory (by default, +src/test/resources/__files). To customize this location you can set the files +attribute in the @AutoConfigureWireMock annotation to the location of the parent +directory (in other words, __files is a subdirectory). You can use Spring resource +notation to refer to file:…​ or classpath:…​ locations. Generic URLs are not +supported. A list of values can be given, in which case WireMock resolves the first file +that exists when it needs to find a response body. + +When you configure the files root, it also affects the +automatic loading of stubs, because they come from the root location +in a subdirectory called "mappings". The value of files has no +effect on the stubs loaded explicitly from the stubs attribute. + +
    +
    +Alternative: Using JUnit Rules +For a more conventional WireMock experience, you can use JUnit @Rules to start and stop +the server. To do so, use the WireMockSpring convenience class to obtain an Options +instance, as shown in the following example: +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class WiremockForDocsClassRuleTests { + + // Start WireMock on some dynamic port + // for some reason `dynamicPort()` is not working properly + @ClassRule + public static WireMockClassRule wiremock = new WireMockClassRule( + WireMockSpring.options().dynamicPort()); + + // A service that calls out over HTTP to wiremock's port + @Autowired + private Service service; + + @Before + public void setup() { + this.service.setBase("http://localhost:" + wiremock.port()); + } + + // Using the WireMock APIs in the normal way: + @Test + public void contextLoads() throws Exception { + // Stubbing WireMock + wiremock.stubFor(get(urlEqualTo("/resource")).willReturn(aResponse() + .withHeader("Content-Type", "text/plain").withBody("Hello World!"))); + // We're asserting if WireMock responded properly + assertThat(this.service.go()).isEqualTo("Hello World!"); + } + +} +The @ClassRule means that the server shuts down after all the methods in this class +have been run. +
    +
    +Relaxed SSL Validation for Rest Template +WireMock lets you stub a "secure" server with an "https" URL protocol. If your +application wants to contact that stub server in an integration test, it will find that +the SSL certificates are not valid (the usual problem with self-installed certificates). +The best option is often to re-configure the client to use "http". If that’s not an +option, you can ask Spring to configure an HTTP client that ignores SSL validation errors +(do so only for tests, of course). +To make this work with minimum fuss, you need to be using the Spring Boot +RestTemplateBuilder in your app, as shown in the following example: +@Bean +public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); +} +You need RestTemplateBuilder because the builder is passed through callbacks to +initialize it, so the SSL validation can be set up in the client at that point. This +happens automatically in your test if you are using the @AutoConfigureWireMock +annotation or the stub runner. If you use the JUnit @Rule approach, you need to add the +@AutoConfigureHttpClient annotation as well, as shown in the following example: +@RunWith(SpringRunner.class) +@SpringBootTest("app.baseUrl=https://localhost:6443") +@AutoConfigureHttpClient +public class WiremockHttpsServerApplicationTests { + + @ClassRule + public static WireMockClassRule wiremock = new WireMockClassRule( + WireMockSpring.options().httpsPort(6443)); +... +} +If you are using spring-boot-starter-test, you have the Apache HTTP client on the +classpath and it is selected by the RestTemplateBuilder and configured to ignore SSL +errors. If you use the default java.net client, you do not need the annotation (but it +won’t do any harm). There is no support currently for other clients, but it may be added +in future releases. +To disable the custom RestTemplateBuilder, set the wiremock.rest-template-ssl-enabled +property to false. +
    +
    +WireMock and Spring MVC Mocks +Spring Cloud Contract provides a convenience class that can load JSON WireMock stubs into +a Spring MockRestServiceServer. The following code shows an example: +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +public class WiremockForDocsMockServerApplicationTests { + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private Service service; + + @Test + public void contextLoads() throws Exception { + // will read stubs classpath + MockRestServiceServer server = WireMockRestServiceServer.with(this.restTemplate) + .baseUrl("https://example.org").stubs("classpath:/stubs/resource.json") + .build(); + // We're asserting if WireMock responded properly + assertThat(this.service.go()).isEqualTo("Hello World"); + server.verify(); + } + +} +The baseUrl value is prepended to all mock calls, and the stubs() method takes a stub +path resource pattern as an argument. In the preceding example, the stub defined at +/stubs/resource.json is loaded into the mock server. If the RestTemplate is asked to +visit https://example.org/, it gets the responses as being declared at that URL. More +than one stub pattern can be specified, and each one can be a directory (for a recursive +list of all ".json"), a fixed filename (as in the example above), or an Ant-style +pattern. The JSON format is the normal WireMock format, which you can read about in the +WireMock website. +Currently, the Spring Cloud Contract Verifier supports Tomcat, Jetty, and Undertow as +Spring Boot embedded servers, and Wiremock itself has "native" support for a particular +version of Jetty (currently 9.2). To use the native Jetty, you need to add the native +Wiremock dependencies and exclude the Spring Boot container (if there is one). +
    +
    +Customization of WireMock configuration +You can register a bean of org.springframework.cloud.contract.wiremock.WireMockConfigurationCustomizer type +in order to customize the WireMock configuration (e.g. add custom transformers). +Example: + @Bean + WireMockConfigurationCustomizer optionsCustomizer() { + return new WireMockConfigurationCustomizer() { + @Override + public void customize(WireMockConfiguration options) { +// perform your customization here + } + }; + } +
    +
    +Generating Stubs using REST Docs +Spring REST Docs can be used to generate +documentation (for example in Asciidoctor format) for an HTTP API with Spring MockMvc +or WebTestClient or Rest Assured. At the same time that you generate documentation for your API, you can also +generate WireMock stubs by using Spring Cloud Contract WireMock. To do so, write your +normal REST Docs test cases and use @AutoConfigureRestDocs to have stubs be +automatically generated in the REST Docs output directory. The following code shows an +example using MockMvc: +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureRestDocs(outputDir = "target/snippets") +@AutoConfigureMockMvc +public class ApplicationTests { + + @Autowired + private MockMvc mockMvc; + + @Test + public void contextLoads() throws Exception { + mockMvc.perform(get("/resource")) + .andExpect(content().string("Hello World")) + .andDo(document("resource")); + } +} +This test generates a WireMock stub at "target/snippets/stubs/resource.json". It matches +all GET requests to the "/resource" path. The same example with WebTestClient (used +for testing Spring WebFlux applications) would look like this: +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureRestDocs(outputDir = "target/snippets") +@AutoConfigureWebTestClient +public class ApplicationTests { + + @Autowired + private WebTestClient client; + + @Test + public void contextLoads() throws Exception { + client.get().uri("/resource").exchange() + .expectBody(String.class).isEqualTo("Hello World") + .consumeWith(document("resource")); + } +} +Without any additional configuration, these tests create a stub with a request matcher +for the HTTP method and all headers except "host" and "content-length". To match the +request more precisely (for example, to match the body of a POST or PUT), we need to +explicitly create a request matcher. Doing so has two effects: + + +Creating a stub that matches only in the way you specify. + + +Asserting that the request in the test case also matches the same conditions. + + +The main entry point for this feature is WireMockRestDocs.verify(), which can be used +as a substitute for the document() convenience method, as shown in the following +example: +import static org.springframework.cloud.contract.wiremock.restdocs.WireMockRestDocs.verify; +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureRestDocs(outputDir = "target/snippets") +@AutoConfigureMockMvc +public class ApplicationTests { + + @Autowired + private MockMvc mockMvc; + + @Test + public void contextLoads() throws Exception { + mockMvc.perform(post("/resource") + .content("{\"id\":\"123456\",\"message\":\"Hello World\"}")) + .andExpect(status().isOk()) + .andDo(verify().jsonPath("$.id")) + .andDo(document("resource")); + } +} +This contract specifies that any valid POST with an "id" field receives the response +defined in this test. You can chain together calls to .jsonPath() to add additional +matchers. If JSON Path is unfamiliar, The JayWay +documentation can help you get up to speed. The WebTestClient version of this test +has a similar verify() static helper that you insert in the same place. +Instead of the jsonPath and contentType convenience methods, you can also use the +WireMock APIs to verify that the request matches the created stub, as shown in the +following example: +@Test +public void contextLoads() throws Exception { + mockMvc.perform(post("/resource") + .content("{\"id\":\"123456\",\"message\":\"Hello World\"}")) + .andExpect(status().isOk()) + .andDo(verify() + .wiremock(WireMock.post( + urlPathEquals("/resource")) + .withRequestBody(matchingJsonPath("$.id"))) + .andDo(document("post-resource")); +} +The WireMock API is rich. You can match headers, query parameters, and request body by +regex as well as by JSON path. These features can be used to create stubs with a wider +range of parameters. The above example generates a stub resembling the following example: + +post-resource.json + +{ + "request" : { + "url" : "/resource", + "method" : "POST", + "bodyPatterns" : [ { + "matchesJsonPath" : "$.id" + }] + }, + "response" : { + "status" : 200, + "body" : "Hello World", + "headers" : { + "X-Application-Context" : "application:-1", + "Content-Type" : "text/plain" + } + } +} + + + +You can use either the wiremock() method or the jsonPath() and contentType() +methods to create request matchers, but you can’t use both approaches. + +On the consumer side, you can make the resource.json generated earlier in this section +available on the classpath (by +<<publishing-stubs-as-jars], for example). After that, you can create a stub using WireMock in a +number of different ways, including by using +@AutoConfigureWireMock(stubs="classpath:resource.json"), as described earlier in this +document. +
    +
    +Generating Contracts by Using REST Docs +You can also generate Spring Cloud Contract DSL files and documentation with Spring REST +Docs. If you do so in combination with Spring Cloud WireMock, you get both the contracts +and the stubs. +Why would you want to use this feature? Some people in the community asked questions +about a situation in which they would like to move to DSL-based contract definition, +but they already have a lot of Spring MVC tests. Using this feature lets you generate +the contract files that you can later modify and move to folders (defined in your +configuration) so that the plugin finds them. + +You might wonder why this functionality is in the WireMock module. The functionality +is there because it makes sense to generate both the contracts and the stubs. + +Consider the following test: + this.mockMvc + .perform(post("/foo").accept(MediaType.APPLICATION_PDF) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"foo\": 23, \"bar\" : \"baz\" }")) + .andExpect(status().isOk()).andExpect(content().string("bar")) + // first WireMock + .andDo(WireMockRestDocs.verify().jsonPath("$[?(@.foo >= 20)]") + .jsonPath("$[?(@.bar in ['baz','bazz','bazzz'])]") + .contentType(MediaType.valueOf("application/json"))) + // then Contract DSL documentation + .andDo(document("index", SpringCloudContractRestDocs.dslContract())); +The preceding test creates the stub presented in the previous section, generating both +the contract and a documentation file. +The contract is called index.groovy and might look like the following example: +import org.springframework.cloud.contract.spec.Contract + +Contract.make { + request { + method 'POST' + url '/foo' + body(''' + {"foo": 23 } + ''') + headers { + header('''Accept''', '''application/json''') + header('''Content-Type''', '''application/json''') + } + } + response { + status OK() + body(''' + bar + ''') + headers { + header('''Content-Type''', '''application/json;charset=UTF-8''') + header('''Content-Length''', '''3''') + } + testMatchers { + jsonPath('$[?(@.foo >= 20)]', byType()) + } + } +} +The generated document (formatted in Asciidoc in this case) contains a formatted +contract. The location of this file would be index/dsl-contract.adoc. +
    +
    + +Migrations + +For up to date migration guides please visit +the project’s wiki page. + +This section covers migrating from one version of Spring Cloud Contract Verifier to the +next version. It covers the following versions upgrade paths: +
    +1.0.x → 1.1.x +This section covers upgrading from version 1.0 to version 1.1. +
    +New structure of generated stubs +In 1.1.x we have introduced a change to the structure of generated stubs. If you have +been using the @AutoConfigureWireMock notation to use the stubs from the classpath, +it no longer works. The following example shows how the @AutoConfigureWireMock notation +used to work: +@AutoConfigureWireMock(stubs = "classpath:/customer-stubs/mappings", port = 8084) +You must either change the location of the stubs to: +classpath:…​/META-INF/groupId/artifactId/version/mappings or use the new +classpath-based @AutoConfigureStubRunner, as shown in the following example: +@AutoConfigureWireMock(stubs = "classpath:customer-stubs/META-INF/travel.components/customer-contract/1.0.2-SNAPSHOT/mappings/", port = 8084) +If you do not want to use @AutoConfigureStubRunner and you want to remain with the old +structure, set your plugin tasks accordingly. The following example would work for the +structure presented in the previous snippet. + +Maven + +<!-- start of pom.xml --> + +<properties> + <!-- we don't want the verifier to do a jar for us --> + <spring.cloud.contract.verifier.skip>true</spring.cloud.contract.verifier.skip> +</properties> + +<!-- ... --> + +<!-- You need to set up the assembly plugin --> +<build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <id>stub</id> + <phase>prepare-package</phase> + <goals> + <goal>single</goal> + </goals> + <inherited>false</inherited> + <configuration> + <attach>true</attach> + <descriptor>$../../../../src/assembly/stub.xml</descriptor> + </configuration> + </execution> + </executions> + </plugin> + </plugins> +</build> +<!-- end of pom.xml --> + +<!-- start of stub.xml--> + +<assembly + xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 https://maven.apache.org/xsd/assembly-1.1.3.xsd"> + <id>stubs</id> + <formats> + <format>jar</format> + </formats> + <includeBaseDirectory>false</includeBaseDirectory> + <fileSets> + <fileSet> + <directory>${project.build.directory}/snippets/stubs</directory> + <outputDirectory>customer-stubs/mappings</outputDirectory> + <includes> + <include>**/*</include> + </includes> + </fileSet> + <fileSet> + <directory>$../../../../src/test/resources/contracts</directory> + <outputDirectory>customer-stubs/contracts</outputDirectory> + <includes> + <include>**/*.groovy</include> + </includes> + </fileSet> + </fileSets> +</assembly> + +<!-- end of stub.xml--> + + + +Gradle + +task copyStubs(type: Copy, dependsOn: 'generateWireMockClientStubs') { +// Preserve directory structure from 1.0.X of spring-cloud-contract + from "${project.buildDir}/resources/main/customer-stubs/META-INF/${project.group}/${project.name}/${project.version}" + into "${project.buildDir}/resources/main/customer-stubs" +} + + +
    +
    +
    +1.1.x → 1.2.x +This section covers upgrading from version 1.1 to version 1.2. +
    +Custom <literal>HttpServerStub</literal> +HttpServerStub includes a method that was not in version 1.1. The method is +String registeredMappings() If you have classes that implement HttpServerStub, you +now have to implement the registeredMappings() method. It should return a String +representing all mappings available in a single HttpServerStub. +See issue 355 for more +detail. +
    +
    +New packages for generated tests +The flow for setting the generated tests package name will look like this: + + +Set basePackageForTests + + +If basePackageForTests was not set, pick the package from baseClassForTests + + +If baseClassForTests was not set, pick packageWithBaseClasses + + +If nothing got set, pick the default value: +org.springframework.cloud.contract.verifier.tests + + +See issue 260 for more +detail. +
    +
    +New Methods in TemplateProcessor +In order to add support for fromRequest.path, the following methods had to be added to the +TemplateProcessor interface: + + +path() + + +path(int index) + + +See issue 388 for more +detail. +
    +
    +RestAssured 3.0 +Rest Assured, used in the generated test classes, got bumped to 3.0. If +you manually set versions of Spring Cloud Contract and the release train +you might see the following exception: +Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile (default-testCompile) on project some-project: Compilation failure: Compilation failure: +[ERROR] /some/path/SomeClass.java:[4,39] package com.jayway.restassured.response does not exist +This exception will occur due to the fact that the tests got generated with +an old version of plugin and at test execution time you have an incompatible +version of the release train (and vice versa). +Done via issue 267 +
    +
    +
    +1.2.x → 2.0.x + +
    +
    + +Links +The following links may be helpful when working with Spring Cloud Contract: + + +Spring Cloud Contract Github +Repository + + +Spring Cloud +Contract Samples + + +Spring Cloud Contract Gitter + + +Spring Cloud Contract WJUG Presentation by +Marcin Grzejszczak + + + +
    + +Spring Cloud Vault + +© 2016-2019 The original authors. + +Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. + +Spring Cloud Vault Config provides client-side support for externalized configuration in a distributed system. With HashiCorp’s Vault you have a central place to manage external secret properties for applications across all environments. Vault can manage static and dynamic secrets such as username/password for remote applications/resources and provide credentials for external services such as MySQL, PostgreSQL, Apache Cassandra, MongoDB, Consul, AWS and more. + + +Quick Start +Prerequisites +To get started with Vault and this guide you need a +*NIX-like operating systems that provides: + + +wget, openssl and unzip + + +at least Java 7 and a properly configured JAVA_HOME environment variable + + +Install Vault +$ src/test/bash/install_vault.sh +Create SSL certificates for Vault +$ src/test/bash/create_certificates.sh + +create_certificates.sh creates certificates in work/ca and a JKS truststore work/keystore.jks. If you want to run Spring Cloud Vault using this quickstart guide you need to configure the truststore the spring.cloud.vault.ssl.trust-store property to file:work/keystore.jks. + +Start Vault server +$ src/test/bash/local_run_vault.sh +Vault is started listening on 0.0.0.0:8200 using the inmem storage and +https. +Vault is sealed and not initialized when starting up. + +If you want to run tests, leave Vault uninitialized. The tests will +initialize Vault and create a root token 00000000-0000-0000-0000-000000000000. + +If you want to use Vault for your application or give it a try then you need to initialize it first. +$ export VAULT_ADDR="https://localhost:8200" +$ export VAULT_SKIP_VERIFY=true # Don't do this for production +$ vault init +You should see something like: +Key 1: 7149c6a2e16b8833f6eb1e76df03e47f6113a3288b3093faf5033d44f0e70fe701 +Key 2: 901c534c7988c18c20435a85213c683bdcf0efcd82e38e2893779f152978c18c02 +Key 3: 03ff3948575b1165a20c20ee7c3e6edf04f4cdbe0e82dbff5be49c63f98bc03a03 +Key 4: 216ae5cc3ddaf93ceb8e1d15bb9fc3176653f5b738f5f3d1ee00cd7dccbe926e04 +Key 5: b2898fc8130929d569c1677ee69dc5f3be57d7c4b494a6062693ce0b1c4d93d805 +Initial Root Token: 19aefa97-cccc-bbbb-aaaa-225940e63d76 + +Vault initialized with 5 keys and a key threshold of 3. Please +securely distribute the above keys. When the Vault is re-sealed, +restarted, or stopped, you must provide at least 3 of these keys +to unseal it again. + +Vault does not store the master key. Without at least 3 keys, +your Vault will remain permanently sealed. +Vault will initialize and return a set of unsealing keys and the root token. +Pick 3 keys and unseal Vault. Store the Vault token in the VAULT_TOKEN + environment variable. +$ vault unseal (Key 1) +$ vault unseal (Key 2) +$ vault unseal (Key 3) +$ export VAULT_TOKEN=(Root token) +# Required to run Spring Cloud Vault tests after manual initialization +$ vault token-create -id="00000000-0000-0000-0000-000000000000" -policy="root" +Spring Cloud Vault accesses different resources. By default, the secret +backend is enabled which accesses secret config settings via JSON endpoints. +The HTTP service has resources in the form: +/secret/{application}/{profile} +/secret/{application} +/secret/{defaultContext}/{profile} +/secret/{defaultContext} +where the "application" is injected as the spring.application.name in the +SpringApplication (i.e. what is normally "application" in a regular +Spring Boot app), "profile" is an active profile (or comma-separated +list of properties). Properties retrieved from Vault will be used "as-is" +without further prefixing of the property names. + + +Client Side Usage +To use these features in an application, just build it as a Spring +Boot application that depends on spring-cloud-vault-config (e.g. see +the test cases). Example Maven configuration: + +pom.xml +<parent> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-parent</artifactId> + <version>2.0.0.RELEASE</version> + <relativePath /> <!-- lookup parent from repository --> +</parent> + +<dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-vault-config</artifactId> + <version>{project-version}</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-test</artifactId> + <scope>test</scope> + </dependency> +</dependencies> + +<build> + <plugins> + <plugin> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-maven-plugin</artifactId> + </plugin> + </plugins> +</build> + +<!-- repositories also needed for snapshots and milestones --> + +Then you can create a standard Spring Boot application, like this simple HTTP server: + +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + +When it runs it will pick up the external configuration from the +default local Vault server on port 8200 if it is running. To modify +the startup behavior you can change the location of the Vault server +using bootstrap.properties (like application.properties but for +the bootstrap phase of an application context), e.g. + +bootstrap.yml +spring.cloud.vault: + host: localhost + port: 8200 + scheme: https + uri: https://localhost:8200 + connection-timeout: 5000 + read-timeout: 15000 + config: + order: -10 + + + +host sets the hostname of the Vault host. The host name will be used +for SSL certificate validation + + +port sets the Vault port + + +scheme setting the scheme to http will use plain HTTP. +Supported schemes are http and https. + + +uri configure the Vault endpoint with an URI. Takes precedence over host/port/scheme configuration + + +connection-timeout sets the connection timeout in milliseconds + + +read-timeout sets the read timeout in milliseconds + + +config.order sets the order for the property source + + +Enabling further integrations requires additional dependencies and +configuration. Depending on how you have set up Vault you might need +additional configuration like +SSL and +authentication. +If the application imports the spring-boot-starter-actuator project, the +status of the vault server will be available via the /health endpoint. +The vault health indicator can be enabled or disabled through the property management.health.vault.enabled (default to true). +
    +Authentication +Vault requires an authentication mechanism to authorize client requests. +Spring Cloud Vault supports multiple authentication mechanisms to authenticate applications with Vault. +For a quickstart, use the root token printed by the Vault initialization. + +bootstrap.yml +spring.cloud.vault: + token: 19aefa97-cccc-bbbb-aaaa-225940e63d76 + + +Consider carefully your security requirements. Static token authentication is fine if you want quickly get started with Vault, but a static token is not protected any further. Any disclosure to unintended parties allows Vault use with the associated token roles. + +
    +
    + +Authentication methods +Different organizations have different requirements for security +and authentication. Vault reflects that need by shipping multiple authentication +methods. Spring Cloud Vault supports token and AppId authentication. +
    +Token authentication +Tokens are the core method for authentication within Vault. +Token authentication requires a static token to be provided using the +Bootstrap Application Context. + +Token authentication is the default authentication method. +If a token is disclosed an unintended party gains access to Vault and +can access secrets for the intended client. + + +bootstrap.yml +spring.cloud.vault: + authentication: TOKEN + token: 00000000-0000-0000-0000-000000000000 + + + +authentication setting this value to TOKEN selects the Token +authentication method + + +token sets the static token to use + + +See also: Vault Documentation: Tokens +
    +
    +AppId authentication +Vault supports AppId +authentication that consists of two hard to guess tokens. The AppId +defaults to spring.application.name that is statically configured. +The second token is the UserId which is a part determined by the application, +usually related to the runtime environment. IP address, Mac address or a +Docker container name are good examples. Spring Cloud Vault Config supports +IP address, Mac address and static UserId’s (e.g. supplied via System properties). +The IP and Mac address are represented as Hex-encoded SHA256 hash. +IP address-based UserId’s use the local host’s IP address. + +bootstrap.yml using SHA256 IP-Address UserId’s +spring.cloud.vault: + authentication: APPID + app-id: + user-id: IP_ADDRESS + + + +authentication setting this value to APPID selects the AppId +authentication method + + +app-id-path sets the path of the AppId mount to use + + +user-id sets the UserId method. Possible values are IP_ADDRESS, +MAC_ADDRESS or a class name implementing a custom AppIdUserIdMechanism + + +The corresponding command to generate the IP address UserId from a command line is: +$ echo -n 192.168.99.1 | sha256sum + +Including the line break of echo leads to a different hash value +so make sure to include the -n flag. + +Mac address-based UserId’s obtain their network device from the +localhost-bound device. The configuration also allows specifying +a network-interface hint to pick the right device. The value of +network-interface is optional and can be either an interface +name or interface index (0-based). + +bootstrap.yml using SHA256 Mac-Address UserId’s +spring.cloud.vault: + authentication: APPID + app-id: + user-id: MAC_ADDRESS + network-interface: eth0 + + + +network-interface sets network interface to obtain the physical address + + +The corresponding command to generate the IP address UserId from a command line is: +$ echo -n 0AFEDE1234AC | sha256sum + +The Mac address is specified uppercase and without colons. +Including the line break of echo leads to a different hash value +so make sure to include the -n flag. + +
    +Custom UserId +The UserId generation is an open mechanism. You can set +spring.cloud.vault.app-id.user-id to any string and the configured +value will be used as static UserId. +A more advanced approach lets you set spring.cloud.vault.app-id.user-id to a +classname. This class must be on your classpath and must implement +the org.springframework.cloud.vault.AppIdUserIdMechanism interface +and the createUserId method. Spring Cloud Vault will obtain the UserId +by calling createUserId each time it authenticates using AppId to +obtain a token. + +bootstrap.yml +spring.cloud.vault: + authentication: APPID + app-id: + user-id: com.examlple.MyUserIdMechanism + + +MyUserIdMechanism.java +public class MyUserIdMechanism implements AppIdUserIdMechanism { + + @Override + public String createUserId() { + String userId = ... + return userId; + } +} + +See also: Vault Documentation: Using the App ID auth backend +
    +
    +
    +AppRole authentication +AppRole is intended for machine +authentication, like the deprecated (since Vault 0.6.1) . +AppRole authentication consists of two hard to guess (secret) tokens: RoleId and SecretId. +Spring Vault supports various AppRole scenarios (push/pull mode and wrapped). +RoleId and optionally SecretId must be provided by configuration, +Spring Vault will not look up these or create a custom SecretId. + +bootstrap.yml with AppRole authentication properties +spring.cloud.vault: + authentication: APPROLE + app-role: + role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52 + +The following scenarios are supported along the required configuration details: + +Configuration + + + + + + + + +Method +RoleId +SecretId +RoleName +Token + + +Provided RoleId/SecretId +Provided +Provided + + + + +Provided RoleId without SecretId +Provided + + + + + +Provided RoleId, Pull SecretId +Provided +Provided +Provided +Provided + + +Pull RoleId, provided SecretId + +Provided +Provided +Provided + + +Full Pull Mode + + +Provided +Provided + + +Wrapped + + + +Provided + + +Wrapped RoleId, provided SecretId +Provided + + +Provided + + +Provided RoleId, wrapped SecretId + +Provided + +Provided + + + +
    + +Pull/Push/Wrapped Matrix + + + + + + +RoleId +SecretId +Supported + + +Provided +Provided + + + +Provided +Pull + + + +Provided +Wrapped + + + +Provided +Absent + + + +Pull +Provided + + + +Pull +Pull + + + +Pull +Wrapped + + + +Pull +Absent + + + +Wrapped +Provided + + + +Wrapped +Pull + + + +Wrapped +Wrapped + + + +Wrapped +Absent + + + + +
    + +You can use still all combinations of push/pull/wrapped modes by providing a configured AppRoleAuthentication bean within the bootstrap context. Spring Cloud Vault cannot derive all possible AppRole combinations from the configuration properties. + + +AppRole authentication is limited to simple pull mode using reactive infrastructure. Full pull mode is not yet supported. Using Spring Cloud Vault with the Spring WebFlux stack enables Vault’s reactive auto-configuration which can be disabled by setting spring.cloud.vault.reactive.enabled=false. + + +bootstrap.yml with all AppRole authentication properties +spring.cloud.vault: + authentication: APPROLE + app-role: + role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52 + secret-id: 1696536f-1976-73b1-b241-0b4213908d39 + role: my-role + app-role-path: approle + + + +role-id sets the RoleId. + + +secret-id sets the SecretId. SecretId can be omitted if AppRole is configured without requiring SecretId (See bind_secret_id). + + +role: sets the AppRole name for pull mode. + + +app-role-path sets the path of the approle authentication mount to use. + + +See also: Vault Documentation: Using the AppRole auth backend +
    +
    +AWS-EC2 authentication +The aws-ec2 +auth backend provides a secure introduction mechanism +for AWS EC2 instances, allowing automated retrieval of a Vault +token. Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats AWS as a Trusted Third Party and uses the +cryptographically signed dynamic metadata information that uniquely +represents each EC2 instance. + +bootstrap.yml using AWS-EC2 Authentication +spring.cloud.vault: + authentication: AWS_EC2 + +AWS-EC2 authentication enables nonce by default to follow +the Trust On First Use (TOFU) principle. Any unintended party that +gains access to the PKCS#7 identity metadata can authenticate +against Vault. +During the first login, Spring Cloud Vault generates a nonce +that is stored in the auth backend aside the instance Id. +Re-authentication requires the same nonce to be sent. Any other +party does not have the nonce and can raise an alert in Vault for +further investigation. +The nonce is kept in memory and is lost during application restart. +You can configure a static nonce with spring.cloud.vault.aws-ec2.nonce. +AWS-EC2 authentication roles are optional and default to the AMI. +You can configure the authentication role by setting the +spring.cloud.vault.aws-ec2.role property. + +bootstrap.yml with configured role +spring.cloud.vault: + authentication: AWS_EC2 + aws-ec2: + role: application-server + + +bootstrap.yml with all AWS EC2 authentication properties +spring.cloud.vault: + authentication: AWS_EC2 + aws-ec2: + role: application-server + aws-ec2-path: aws-ec2 + identity-document: http://... + nonce: my-static-nonce + + + +authentication setting this value to AWS_EC2 selects the AWS EC2 +authentication method + + +role sets the name of the role against which the login is being attempted. + + +aws-ec2-path sets the path of the AWS EC2 mount to use + + +identity-document sets URL of the PKCS#7 AWS EC2 identity document + + +nonce used for AWS-EC2 authentication. An empty nonce defaults to nonce generation + + +See also: Vault Documentation: Using the aws auth backend +
    +
    +AWS-IAM authentication +The aws backend provides a secure +authentication mechanism for AWS IAM roles, allowing the automatic authentication with +vault based on the current IAM role of the running application. + Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats AWS as a Trusted Third Party and uses the +4 pieces of information signed by the caller with their IAM credentials + to verify that the caller is indeed using that IAM role. +The current IAM role the application is running in is automatically calculated. +If you are running your application on AWS ECS then the application +will use the IAM role assigned to the ECS task of the running container. +If you are running your application naked on top of an EC2 instance then +the IAM role used will be the one assigned to the EC2 instance. +When using the AWS-IAM authentication you must create a role in Vault +and assign it to your IAM role. An empty role defaults to +the friendly name the current IAM role. + +bootstrap.yml with required AWS-IAM Authentication properties +spring.cloud.vault: + authentication: AWS_IAM + + +bootstrap.yml with all AWS-IAM Authentication properties +spring.cloud.vault: + authentication: AWS_IAM + aws-iam: + role: my-dev-role + aws-path: aws + server-id: some.server.name + + + +role sets the name of the role against which the login is being attempted. This should be bound to your IAM role. If one is not supplied then the friendly name of the current IAM user will be used as the vault role. + + +aws-path sets the path of the AWS mount to use + + +server-id sets the value to use for the X-Vault-AWS-IAM-Server-ID header preventing certain types of replay attacks. + + +AWS-IAM requires the AWS Java SDK dependency (com.amazonaws:aws-java-sdk-core) +as the authentication implementation uses AWS SDK types for credentials and request signing. +See also: Vault Documentation: Using the aws auth backend +
    +
    +Azure MSI authentication +The azure +auth backend provides a secure introduction mechanism +for Azure VM instances, allowing automated retrieval of a Vault +token. Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats Azure as a Trusted Third Party and uses the +managed service identity and instance metadata information that can be +bound to a VM instance. + +bootstrap.yml with required Azure Authentication properties +spring.cloud.vault: + authentication: AZURE_MSI + azure-msi: + role: my-dev-role + + +bootstrap.yml with all Azure Authentication properties +spring.cloud.vault: + authentication: AZURE_MSI + azure-msi: + role: my-dev-role + azure-path: azure + + + +role sets the name of the role against which the login is being attempted. + + +azure-path sets the path of the Azure mount to use + + +Azure MSI authentication fetches environmental details about the virtual machine +(subscription Id, resource group, VM name) from the instance metadata service. +See also: Vault Documentation: Using the azure auth backend +
    +
    +TLS certificate authentication +The cert auth backend allows authentication using SSL/TLS client +certificates that are either signed by a CA or self-signed. +To enable cert authentication you need to: + + +Use SSL, see + + +Configure a Java Keystore that contains the client +certificate and the private key + + +Set the spring.cloud.vault.authentication to CERT + + + +bootstrap.yml +spring.cloud.vault: + authentication: CERT + ssl: + key-store: classpath:keystore.jks + key-store-password: changeit + cert-auth-path: cert + +See also: Vault Documentation: Using the Cert auth backend +
    +
    +Cubbyhole authentication +Cubbyhole authentication uses Vault primitives to provide a secured authentication +workflow. Cubbyhole authentication uses tokens as primary login method. +An ephemeral token is used to obtain a second, login VaultToken from Vault’s +Cubbyhole secret backend. The login token is usually longer-lived and used to +interact with Vault. The login token will be retrieved from a wrapped +response stored at /cubbyhole/response. +Creating a wrapped token + +Response Wrapping for token creation requires Vault 0.6.0 or higher. + + +Creating and storing tokens +$ vault token-create -wrap-ttl="10m" +Key Value +--- ----- +wrapping_token: 397ccb93-ff6c-b17b-9389-380b01ca2645 +wrapping_token_ttl: 0h10m0s +wrapping_token_creation_time: 2016-09-18 20:29:48.652957077 +0200 CEST +wrapped_accessor: 46b6aebb-187f-932a-26d7-4f3d86a68319 + + +bootstrap.yml +spring.cloud.vault: + authentication: CUBBYHOLE + token: 397ccb93-ff6c-b17b-9389-380b01ca2645 + +See also: + + +Vault Documentation: Tokens + + +Vault Documentation: Cubbyhole Secret Backend + + +Vault Documentation: Response Wrapping + + +
    +
    +GCP-GCE authentication +The gcp +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials. +GCP GCE (Google Compute Engine) authentication creates a signature in the form of a +JSON Web Token (JWT) for a service account. A JWT for a Compute Engine instance +is obtained from the GCE metadata service using Instance identification. +This API creates a JSON Web Token that can be used to confirm the instance identity. +Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats GCP as a Trusted Third Party and uses the +cryptographically signed dynamic metadata information that uniquely +represents each GCP service account. + +bootstrap.yml with required GCP-GCE Authentication properties +spring.cloud.vault: + authentication: GCP_GCE + gcp-gce: + role: my-dev-role + + +bootstrap.yml with all GCP-GCE Authentication properties +spring.cloud.vault: + authentication: GCP_GCE + gcp-gce: + gcp-path: gcp + role: my-dev-role + service-account: my-service@projectid.iam.gserviceaccount.com + + + +role sets the name of the role against which the login is being attempted. + + +gcp-path sets the path of the GCP mount to use + + +service-account allows overriding the service account Id to a specific value. Defaults to the default service account. + + +See also: + + +Vault Documentation: Using the GCP auth backend + + +GCP Documentation: Verifying the Identity of Instances + + +
    +
    +GCP-IAM authentication +The gcp +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials. +GCP IAM authentication creates a signature in the form of a JSON Web Token (JWT) +for a service account. A JWT for a service account is obtained by +calling GCP IAM’s projects.serviceAccounts.signJwt API. The caller authenticates against GCP IAM +and proves thereby its identity. This Vault backend treats GCP as a Trusted Third Party. +IAM credentials can be obtained from either the runtime environment +, specifically the GOOGLE_APPLICATION_CREDENTIALS +environment variable, the Google Compute metadata service, +or supplied externally as e.g. JSON or base64 encoded. +JSON is the preferred form as it carries the project id and +service account identifier required for calling projects.serviceAccounts.signJwt. + +bootstrap.yml with required GCP-IAM Authentication properties +spring.cloud.vault: + authentication: GCP_IAM + gcp-iam: + role: my-dev-role + + +bootstrap.yml with all GCP-IAM Authentication properties +spring.cloud.vault: + authentication: GCP_IAM + gcp-iam: + credentials: + location: classpath:credentials.json + encoded-key: e+KApn0= + gcp-path: gcp + jwt-validity: 15m + project-id: my-project-id + role: my-dev-role + service-account-id: my-service@projectid.iam.gserviceaccount.com + + + +role sets the name of the role against which the login is being attempted. + + +credentials.location path to the credentials resource that contains Google credentials in JSON format. + + +credentials.encoded-key the base64 encoded contents of an OAuth2 account private key in the JSON format. + + +gcp-path sets the path of the GCP mount to use + + +jwt-validity configures the JWT token validity. Defaults to 15 minutes. + + +project-id allows overriding the project Id to a specific value. Defaults to the project Id from the obtained credential. + + +service-account allows overriding the service account Id to a specific value. Defaults to the service account from the obtained credential. + + +GCP IAM authentication requires the Google Cloud Java SDK dependency +(com.google.apis:google-api-services-iam and com.google.auth:google-auth-library-oauth2-http) +as the authentication implementation uses Google APIs for credentials and JWT signing. + +Google credentials require an OAuth 2 token maintaining the token lifecycle. All API +is synchronous therefore, GcpIamAuthentication does not support AuthenticationSteps which is +required for reactive usage. + +See also: + + +Vault Documentation: Using the GCP auth backend + + +GCP Documentation: projects.serviceAccounts.signJwt + + +
    +
    +Kubernetes authentication +Kubernetes authentication mechanism (since Vault 0.8.3) allows to authenticate with Vault using a Kubernetes Service Account Token. +The authentication is role based and the role is bound to a service account name and a namespace. +A file containing a JWT token for a pod’s service account is automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/token. + +bootstrap.yml with all Kubernetes authentication properties +spring.cloud.vault: + authentication: KUBERNETES + kubernetes: + role: my-dev-role + kubernetes-path: kubernetes + service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token + + + +role sets the Role. + + +kubernetes-path sets the path of the Kubernetes mount to use. + + +service-account-token-file sets the location of the file containing the Kubernetes Service Account Token. Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token. + + +See also: + + +Vault Documentation: Kubernetes + + +Kubernetes Documentation: Configure Service Accounts for Pods + + +
    +
    + +Secret Backends +
    +Generic Backend +Spring Cloud Vault supports at the basic level the generic secret +backend. The generic secret backend allows storage of arbitrary +values as key-value store. A single context can store one or many +key-value tuples. Contexts can be organized hierarchically. +Spring Cloud Vault allows using the Application name +and a default context name (application) in combination with active +profiles. +/secret/{application}/{profile} +/secret/{application} +/secret/{default-context}/{profile} +/secret/{default-context} +The application name is determined by the properties: + + +spring.cloud.vault.generic.application-name + + +spring.cloud.vault.application-name + + +spring.application.name + + +Secrets can be obtained from other contexts within the generic backend by adding their +paths to the application name, separated by commas. For example, given the application +name usefulapp,mysql1,projectx/aws, each of these folders will be used: + + +/secret/usefulapp + + +/secret/mysql1 + + +/secret/projectx/aws + + +Spring Cloud Vault adds all active profiles to the list of possible context paths. +No active profiles will skip accessing contexts with a profile name. +Properties are exposed like they are stored (i.e. without additional prefixes). + +spring.cloud.vault: + generic: + enabled: true + backend: secret + profile-separator: '/' + default-context: application + application-name: my-app + + + +enabled setting this value to false disables the secret backend +config usage + + +backend sets the path of the secret mount to use + + +default-context sets the context name used by all applications + + +application-name overrides the application name for use in the generic backend + + +profile-separator separates the profile name from the context in +property sources with profiles + + + +The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable generic secret backend usage for non-versioned key-value backends and kv secret backend usage for versioned key-value backends. + +See also: Vault Documentation: Using the KV Secrets Engine - Version 1 (generic secret backend) +
    +
    +Versioned Key-Value Backend +Spring Cloud Vault supports the versioned Key-Value secret +backend. The key-value backend allows storage of arbitrary +values as key-value store. A single context can store one or many +key-value tuples. Contexts can be organized hierarchically. +Spring Cloud Vault allows using the Application name +and a default context name (application) in combination with active +profiles. +/secret/{application}/{profile} +/secret/{application} +/secret/{default-context}/{profile} +/secret/{default-context} +The application name is determined by the properties: + + +spring.cloud.vault.kv.application-name + + +spring.cloud.vault.application-name + + +spring.application.name + + +Secrets can be obtained from other contexts within the key-value backend by adding their +paths to the application name, separated by commas. For example, given the application +name usefulapp,mysql1,projectx/aws, each of these folders will be used: + + +/secret/usefulapp + + +/secret/mysql1 + + +/secret/projectx/aws + + +Spring Cloud Vault adds all active profiles to the list of possible context paths. +No active profiles will skip accessing contexts with a profile name. +Properties are exposed like they are stored (i.e. without additional prefixes). + +Spring Cloud Vault adds the data/ context between the mount path and the actual context path. + + +spring.cloud.vault: + kv: + enabled: true + backend: secret + profile-separator: '/' + default-context: application + application-name: my-app + + + +enabled setting this value to false disables the secret backend +config usage + + +backend sets the path of the secret mount to use + + +default-context sets the context name used by all applications + + +application-name overrides the application name for use in the generic backend + + +profile-separator separates the profile name from the context in +property sources with profiles + + + +The key-value secret backend can be operated in versioned (v2) and non-versioned (v1) modes. Depending on the mode of operation, a different API is required to access secrets. Make sure to enable generic secret backend usage for non-versioned key-value backends and kv secret backend usage for versioned key-value backends. + +See also: Vault Documentation: Using the KV Secrets Engine - Version 2 (versioned key-value backend) +
    +
    +Consul +Spring Cloud Vault can obtain credentials for HashiCorp Consul. +The Consul integration requires the spring-cloud-vault-config-consul +dependency. + +pom.xml +<dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-vault-config-consul</artifactId> + <version>{project-version}</version> + </dependency> +</dependencies> + +The integration can be enabled by setting +spring.cloud.vault.consul.enabled=true (default false) and +providing the role name with spring.cloud.vault.consul.role=…. +The obtained token is stored in spring.cloud.consul.token +so using Spring Cloud Consul can pick up the generated +credentials without further configuration. You can configure +the property name by setting spring.cloud.vault.consul.token-property. + +spring.cloud.vault: + consul: + enabled: true + role: readonly + backend: consul + token-property: spring.cloud.consul.token + + + +enabled setting this value to true enables the Consul backend config usage + + +role sets the role name of the Consul role definition + + +backend sets the path of the Consul mount to use + + +token-property sets the property name in which the Consul ACL token is stored + + +See also: Vault Documentation: Setting up Consul with Vault +
    +
    +RabbitMQ +Spring Cloud Vault can obtain credentials for RabbitMQ. +The RabbitMQ integration requires the spring-cloud-vault-config-rabbitmq +dependency. + +pom.xml +<dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-vault-config-rabbitmq</artifactId> + <version>{project-version}</version> + </dependency> +</dependencies> + +The integration can be enabled by setting +spring.cloud.vault.rabbitmq.enabled=true (default false) +and providing the role name with spring.cloud.vault.rabbitmq.role=…. +Username and password are stored in spring.rabbitmq.username +and spring.rabbitmq.password so using Spring Boot will pick up the generated +credentials without further configuration. You can configure the property names +by setting spring.cloud.vault.rabbitmq.username-property and +spring.cloud.vault.rabbitmq.password-property. + +spring.cloud.vault: + rabbitmq: + enabled: true + role: readonly + backend: rabbitmq + username-property: spring.rabbitmq.username + password-property: spring.rabbitmq.password + + + +enabled setting this value to true enables the RabbitMQ backend config usage + + +role sets the role name of the RabbitMQ role definition + + +backend sets the path of the RabbitMQ mount to use + + +username-property sets the property name in which the RabbitMQ username is stored + + +password-property sets the property name in which the RabbitMQ password is stored + + +See also: Vault Documentation: Setting up RabbitMQ with Vault +
    +
    +AWS +Spring Cloud Vault can obtain credentials for AWS. +The AWS integration requires the spring-cloud-vault-config-aws +dependency. + +pom.xml +<dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-vault-config-aws</artifactId> + <version>{project-version}</version> + </dependency> +</dependencies> + +The integration can be enabled by setting +spring.cloud.vault.aws=true (default false) +and providing the role name with spring.cloud.vault.aws.role=…. +The access key and secret key are stored in cloud.aws.credentials.accessKey +and cloud.aws.credentials.secretKey so using Spring Cloud AWS will pick up the generated +credentials without further configuration. You can configure the property names +by setting spring.cloud.vault.aws.access-key-property and +spring.cloud.vault.aws.secret-key-property. + +spring.cloud.vault: + aws: + enabled: true + role: readonly + backend: aws + access-key-property: cloud.aws.credentials.accessKey + secret-key-property: cloud.aws.credentials.secretKey + + + +enabled setting this value to true enables the AWS backend config usage + + +role sets the role name of the AWS role definition + + +backend sets the path of the AWS mount to use + + +access-key-property sets the property name in which the AWS access key is stored + + +secret-key-property sets the property name in which the AWS secret key is stored + + +See also: Vault Documentation: Setting up AWS with Vault +
    +
    + +Database backends +Vault supports several database secret backends to generate database +credentials dynamically based on configured roles. This means +services that need to access a database no longer need to configure +credentials: they can request them from Vault, and use Vault’s leasing +mechanism to more easily roll keys. +Spring Cloud Vault integrates with these backends: + + + + + + + + + + + + + + + + + +Using a database secret backend requires to enable the +backend in the configuration and the spring-cloud-vault-config-databases +dependency. +Vault ships since 0.7.1 with a dedicated database secret backend that allows +database integration via plugins. You can use that specific backend by using the +generic database backend. Make sure to specify the appropriate +backend path, e.g. spring.cloud.vault.mysql.role.backend=database. + +pom.xml +<dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-vault-config-databases</artifactId> + <version>{project-version}</version> + </dependency> +</dependencies> + + +Enabling multiple JDBC-compliant databases will generate credentials +and store them by default in the same property keys hence property names for +JDBC secrets need to be configured separately. + +
    +Database +Spring Cloud Vault can obtain credentials for any database listed at +https://www.vaultproject.io/api/secret/databases/index.html. +The integration can be enabled by setting +spring.cloud.vault.database.enabled=true (default false) and +providing the role name with spring.cloud.vault.database.role=…. +While the database backend is a generic one, spring.cloud.vault.database +specifically targets JDBC databases. Username and password are +stored in spring.datasource.username and spring.datasource.password +so using Spring Boot will pick up the generated credentials +for your DataSource without further configuration. +You can configure the property names by setting +spring.cloud.vault.database.username-property and +spring.cloud.vault.database.password-property. + +spring.cloud.vault: + database: + enabled: true + role: readonly + backend: database + username-property: spring.datasource.username + password-property: spring.datasource.password + + + +enabled setting this value to true enables the Database backend config usage + + +role sets the role name of the Database role definition + + +backend sets the path of the Database mount to use + + +username-property sets the property name in which the Database username is stored + + +password-property sets the property name in which the Database password is stored + + +See also: Vault Documentation: Database Secrets backend + +Spring Cloud Vault does not support getting new credentials and +configuring your DataSource with them when the maximum lease time +has been reached. That is, if max_ttl of the Database role in Vault +is set to 24h that means that 24 hours after your application has +started it can no longer authenticate with the database. + +
    +
    +Apache Cassandra + +The cassandra backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as cassandra. + +Spring Cloud Vault can obtain credentials for Apache Cassandra. +The integration can be enabled by setting +spring.cloud.vault.cassandra.enabled=true (default false) and +providing the role name with spring.cloud.vault.cassandra.role=…. +Username and password are stored in spring.data.cassandra.username +and spring.data.cassandra.password so using Spring Boot will pick +up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.cassandra.username-property and +spring.cloud.vault.cassandra.password-property. + +spring.cloud.vault: + cassandra: + enabled: true + role: readonly + backend: cassandra + username-property: spring.data.cassandra.username + password-property: spring.data.cassandra.password + + + +enabled setting this value to true enables the Cassandra backend config usage + + +role sets the role name of the Cassandra role definition + + +backend sets the path of the Cassandra mount to use + + +username-property sets the property name in which the Cassandra username is stored + + +password-property sets the property name in which the Cassandra password is stored + + +See also: Vault Documentation: Setting up Apache Cassandra with Vault +
    +
    +MongoDB + +The mongodb backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as mongodb. + +Spring Cloud Vault can obtain credentials for MongoDB. +The integration can be enabled by setting +spring.cloud.vault.mongodb.enabled=true (default false) and +providing the role name with spring.cloud.vault.mongodb.role=…. +Username and password are stored in spring.data.mongodb.username +and spring.data.mongodb.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.mongodb.username-property and +spring.cloud.vault.mongodb.password-property. + +spring.cloud.vault: + mongodb: + enabled: true + role: readonly + backend: mongodb + username-property: spring.data.mongodb.username + password-property: spring.data.mongodb.password + + + +enabled setting this value to true enables the MongodB backend config usage + + +role sets the role name of the MongoDB role definition + + +backend sets the path of the MongoDB mount to use + + +username-property sets the property name in which the MongoDB username is stored + + +password-property sets the property name in which the MongoDB password is stored + + +See also: Vault Documentation: Setting up MongoDB with Vault +
    +
    +MySQL + +The mysql backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as mysql. +Configuration for spring.cloud.vault.mysql will be removed in a future version. + +Spring Cloud Vault can obtain credentials for MySQL. +The integration can be enabled by setting +spring.cloud.vault.mysql.enabled=true (default false) and +providing the role name with spring.cloud.vault.mysql.role=…. +Username and password are stored in spring.datasource.username +and spring.datasource.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.mysql.username-property and +spring.cloud.vault.mysql.password-property. + +spring.cloud.vault: + mysql: + enabled: true + role: readonly + backend: mysql + username-property: spring.datasource.username + password-property: spring.datasource.password + + + +enabled setting this value to true enables the MySQL backend config usage + + +role sets the role name of the MySQL role definition + + +backend sets the path of the MySQL mount to use + + +username-property sets the property name in which the MySQL username is stored + + +password-property sets the property name in which the MySQL password is stored + + +See also: Vault Documentation: Setting up MySQL with Vault +
    +
    +PostgreSQL + +The postgresql backend has been deprecated in Vault 0.7.1 and +it is recommended to use the database backend and mount it as postgresql. +Configuration for spring.cloud.vault.postgresql will be removed in a future version. + +Spring Cloud Vault can obtain credentials for PostgreSQL. +The integration can be enabled by setting +spring.cloud.vault.postgresql.enabled=true (default false) and +providing the role name with spring.cloud.vault.postgresql.role=…. +Username and password are stored in spring.datasource.username +and spring.datasource.password so using Spring Boot will +pick up the generated credentials without further configuration. +You can configure the property names by setting +spring.cloud.vault.postgresql.username-property and +spring.cloud.vault.postgresql.password-property. + +spring.cloud.vault: + postgresql: + enabled: true + role: readonly + backend: postgresql + username-property: spring.datasource.username + password-property: spring.datasource.password + + + +enabled setting this value to true enables the PostgreSQL backend config usage + + +role sets the role name of the PostgreSQL role definition + + +backend sets the path of the PostgreSQL mount to use + + +username-property sets the property name in which the PostgreSQL username is stored + + +password-property sets the property name in which the PostgreSQL password is stored + + +See also: Vault Documentation: Setting up PostgreSQL with Vault +
    +
    + +Configure <literal>PropertySourceLocator</literal> behavior +Spring Cloud Vault uses property-based configuration to create PropertySources +for generic and discovered secret backends. +Discovered backends provide VaultSecretBackendDescriptor beans to describe the configuration +state to use secret backend as PropertySource. A SecretBackendMetadataFactory is required +to create a SecretBackendMetadata object which contains path, name and property transformation +configuration. +SecretBackendMetadata is used to back a particular PropertySource. +You can register an arbitrary number of beans implementing VaultConfigurer for customization. +Default generic and discovered backend registration is disabled if Spring Cloud Vault discovers +at least one VaultConfigurer bean. You can however enable default registration with +SecretBackendConfigurer.registerDefaultGenericSecretBackends() and SecretBackendConfigurer.registerDefaultDiscoveredSecretBackends(). + +public class CustomizationBean implements VaultConfigurer { + + @Override + public void addSecretBackends(SecretBackendConfigurer configurer) { + + configurer.add("secret/my-application"); + + configurer.registerDefaultGenericSecretBackends(false); + configurer.registerDefaultDiscoveredSecretBackends(true); + } +} + + +All customization is required to happen in the bootstrap context. Add your configuration +classes to META-INF/spring.factories at org.springframework.cloud.bootstrap.BootstrapConfiguration +in your application. + + + +Service Registry Configuration +You can use a DiscoveryClient (such as from Spring Cloud Consul) to locate +a Vault server by setting spring.cloud.vault.discovery.enabled=true (default false). +The net result of that is that your apps need a bootstrap.yml (or an environment variable) +with the appropriate discovery configuration. +The benefit is that the Vault can change its co-ordinates, as long as the discovery service +is a fixed point. The default service id is vault but you can change that on the client with +spring.cloud.vault.discovery.serviceId. +The discovery client implementations all support some kind of metadata map +(e.g. for Eureka we have eureka.instance.metadataMap). Some additional properties of the service +may need to be configured in its service registration metadata so that clients can connect +correctly. Service registries that do not provide details about transport layer security +need to provide a scheme metadata entry to be set either to https or http. +If no scheme is configured and the service is not exposed as secure service, then +configuration defaults to spring.cloud.vault.scheme which is https when it’s not set. + +spring.cloud.vault.discovery: + enabled: true + service-id: my-vault-service + + + +Vault Client Fail Fast +In some cases, it may be desirable to fail startup of a service if +it cannot connect to the Vault Server. If this is the desired +behavior, set the bootstrap configuration property +spring.cloud.vault.fail-fast=true and the client will halt with +an Exception. + +spring.cloud.vault: + fail-fast: true + + + +Vault Client SSL configuration +SSL can be configured declaratively by setting various properties. +You can set either javax.net.ssl.trustStore to configure +JVM-wide SSL settings or spring.cloud.vault.ssl.trust-store +to set SSL settings only for Spring Cloud Vault Config. + +spring.cloud.vault: + ssl: + trust-store: classpath:keystore.jks + trust-store-password: changeit + + + +trust-store sets the resource for the trust-store. SSL-secured Vault +communication will validate the Vault SSL certificate with the specified +trust-store. + + +trust-store-password sets the trust-store password + + +Please note that configuring spring.cloud.vault.ssl.* can be only +applied when either Apache Http Components or the OkHttp client +is on your class-path. + + +Lease lifecycle management (renewal and revocation) +With every secret, Vault creates a lease: +metadata containing information such as a time duration, +renewability, and more. +Vault promises that the data will be valid for the given duration, +or Time To Live (TTL). Once the lease is expired, Vault can +revoke the data, and the consumer of the secret can no longer +be certain that it is valid. +Spring Cloud Vault maintains a lease lifecycle beyond +the creation of login tokens and secrets. That said, +login tokens and secrets associated with a lease +are scheduled for renewal just before the lease expires +until terminal expiry. +Application shutdown revokes obtained login tokens and renewable +leases. +Secret service and database backends (such as MongoDB or MySQL) +usually generate a renewable lease so generated credentials will +be disabled on application shutdown. + +Static tokens are not renewed or revoked. + +Lease renewal and revocation is enabled by default and can +be disabled by setting spring.cloud.vault.config.lifecycle.enabled +to false. This is not recommended as leases can expire and +Spring Cloud Vault cannot longer access Vault or services +using generated credentials and valid credentials remain active +after application shutdown. + +spring.cloud.vault: + config.lifecycle.enabled: true + +See also: Vault Documentation: Lease, Renew, and Revoke + +
    + +Spring Cloud Gateway + +Greenwich.SR5 +This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency. + + +How to Include Spring Cloud Gateway +To include Spring Cloud Gateway in your project use the starter with group org.springframework.cloud +and artifact id spring-cloud-starter-gateway. See the Spring Cloud Project page +for details on setting up your build system with the current Spring Cloud Release Train. +If you include the starter, but, for some reason, you do not want the gateway to be enabled, set spring.cloud.gateway.enabled=false. + +Spring Cloud Gateway is built upon Spring Boot 2.x, +Spring WebFlux, +and Project Reactor. As a consequence +many of the familiar synchronous libraries (Spring Data and Spring Security, for example) and patterns you may +not apply when using Spring Cloud Gateway. If you are unfamiliar with these projects we suggest you +begin by reading their documentation to familiarize yourself with some of the new concepts before +working with Spring Cloud Gateway. + + +Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It does not work in a traditional Servlet Container or built as a WAR. + + + +Glossary + + +Route: Route the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true. + + +Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This allows developers to match on anything from the HTTP request, such as headers or parameters. + + +Filter: These are instances Spring Framework GatewayFilter constructed in with a specific factory. Here, requests and responses can be modified before or after sending the downstream request. + + + + +How It Works + + + + + +Spring Cloud Gateway Diagram + + +Clients make requests to Spring Cloud Gateway. If the Gateway Handler Mapping determines that a request matches a Route, it is sent to the Gateway Web Handler. This handler runs sends the request through a filter chain that is specific to the request. The reason the filters are divided by the dotted line, is that filters may execute logic before the proxy request is sent or after. All "pre" filter logic is executed, then the proxy request is made. After the proxy request is made, the "post" filter logic is executed. + +URIs defined in routes without a port will get a default port set to 80 and 443 for HTTP and HTTPS URIs respectively. + + + +Configuring Route Predicate Factories and Gateway Filter Factories +There are two ways to configure predicates and filters: shortcuts and fully expanded arguments. Most examples below use the shortcut way. +The name and argument names will be listed as code in the first sentance or two of the each section. The arguments are typically listed in the order that would be needed for the shortcut configuration. +
    +Shortcut Configuration +Shortcut configuration is recognized by the filter name, followed by an equals sign (=), followed by argument values separated by commas (,). + +application.yml + +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - Cookie=mycookie,mycookievalue + + +The previous sample defines the Cookie Route Predicate Factory with two arguments, the cookie name, mycookie and the value to match mycookievalue. +
    +
    +Fully Expanded Arguments +Fully expanded arguments appear more like standard yaml configuration with name/value pairs. Typically, there will be a name key and an args key. The args key is a map of key value pairs to configure the predicate or filter. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - name: Cookie + args: + name: mycookie + regexp: mycookievalue + + +This is the full configuration of the shortcut configuration of the Cookie predicate shown above. +
    +
    + +Route Predicate Factories +Spring Cloud Gateway matches routes as part of the Spring WebFlux HandlerMapping infrastructure. Spring Cloud Gateway includes many built-in Route Predicate Factories. All of these predicates match on different attributes of the HTTP request. Multiple Route Predicate Factories can be combined and are combined via logical and. +
    +After Route Predicate Factory +The After Route Predicate Factory takes one parameter, a datetime (which is a java ZonedDateTime). This predicate matches requests that happen after the current datetime. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - After=2017-01-20T17:42:47.789-07:00[America/Denver] + + +This route matches any request after Jan 20, 2017 17:42 Mountain Time (Denver). +
    +
    +Before Route Predicate Factory +The Before Route Predicate Factory takes one parameter, a datetime(which is a java ZonedDateTime). This predicate matches requests that happen before the current datetime. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: before_route + uri: https://example.org + predicates: + - Before=2017-01-20T17:42:47.789-07:00[America/Denver] + + +This route matches any request before Jan 20, 2017 17:42 Mountain Time (Denver). +
    +
    +Between Route Predicate Factory +The Between Route Predicate Factory takes two parameters, datetime1 and datetime2 which are java ZonedDateTime objects. This predicate matches requests that happen after datetime1 and before datetime2. The datetime2 parameter must be after datetime1. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: between_route + uri: https://example.org + predicates: + - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] + + +This route matches any request after Jan 20, 2017 17:42 Mountain Time (Denver) and before Jan 21, 2017 17:42 Mountain Time (Denver). This could be useful for maintenance windows. +
    +
    +Cookie Route Predicate Factory +The Cookie Route Predicate Factory takes two parameters, the cookie name and a regexp (which is a Java regular expression). This predicate matches cookies that have the given name and the value matches the regular expression. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: cookie_route + uri: https://example.org + predicates: + - Cookie=chocolate, ch.p + + +This route matches the request has a cookie named chocolate who’s value matches the ch.p regular expression. +
    +
    +Header Route Predicate Factory +The Header Route Predicate Factory takes two parameters, the header name and a regexp (which is a Java regular expression). This predicate matches with a header that has the given name and the value matches the regular expression. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: header_route + uri: https://example.org + predicates: + - Header=X-Request-Id, \d+ + + +This route matches if the request has a header named X-Request-Id whos value matches the \d+ regular expression (has a value of one or more digits). +
    +
    +Host Route Predicate Factory +The Host Route Predicate Factory takes one parameter: a list of host name patterns. The pattern is an Ant style pattern with . as the separator. This predicates matches the Host header that matches the pattern. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: host_route + uri: https://example.org + predicates: + - Host=**.somehost.org,**.anotherhost.org + + +URI template variables are supported as well, such as {sub}.myhost.org. +This route would match if the request has a Host header has the value www.somehost.org or beta.somehost.org or www.anotherhost.org. +This predicate extracts the URI template variables (like sub defined in the example above) as a map of names and values and places it in the ServerWebExchange.getAttributes() with a key defined in ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE. Those values are then available for use by GatewayFilter Factories +
    +
    +Method Route Predicate Factory +The Method Route Predicate Factory takes a methods argument which is one or more HTTP methods to match. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: method_route + uri: https://example.org + predicates: + - Method=GET,POST + + +This route would match if the request method was a GET or a POST. +
    +
    +Path Route Predicate Factory +The Path Route Predicate Factory takes two parameter: a list of Spring PathMatcher patterns and an optional flag to matchOptionalTrailingSeparator. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: host_route + uri: https://example.org + predicates: + - Path=/foo/{segment},/bar/{segment} + + +This route would match if the request path was, for example: /foo/1 or /foo/bar or /bar/baz. +This predicate extracts the URI template variables (like segment defined in the example above) as a map of names and values and places it in the ServerWebExchange.getAttributes() with a key defined in ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE. Those values are then available for use by GatewayFilter Factories +A utility method is available to make access to these variables easier. +Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange); + +String segment = uriVariables.get("segment"); +
    +
    +Query Route Predicate Factory +The Query Route Predicate Factory takes two parameters: a required param and an optional regexp (which is a Java regular expression). + +application.yml + +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=baz + + +This route would match if the request contained a baz query parameter. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=foo, ba. + + +This route would match if the request contained a foo query parameter whose value matched the ba. regexp, so bar and baz would match. +
    +
    +RemoteAddr Route Predicate Factory +The RemoteAddr Route Predicate Factory takes a list (min size 1) of sources, which are CIDR-notation (IPv4 or IPv6) strings, e.g. 192.168.0.1/16 (where 192.168.0.1 is an IP address and 16 is a subnet mask). + +application.yml + +spring: + cloud: + gateway: + routes: + - id: remoteaddr_route + uri: https://example.org + predicates: + - RemoteAddr=192.168.1.1/24 + + +This route would match if the remote address of the request was, for example, 192.168.1.10. +
    +
    +Weight Route Predicate Factory +The Weight Route Predicate Factory takes two arguments group and weight (an int). The weights are calculated per group. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: weight_high + uri: https://weighthigh.org + predicates: + - Weight=group1, 8 + - id: weight_low + uri: https://weightlow.org + predicates: + - Weight=group1, 2 + + +This route would forward ~80% of traffic to https://weighthigh.org and ~20% of traffic to https://weighlow.org +
    +Modifying the way remote addresses are resolved +By default the RemoteAddr Route Predicate Factory uses the remote address from the incoming request. +This may not match the actual client IP address if Spring Cloud Gateway sits behind a proxy layer. +You can customize the way that the remote address is resolved by setting a custom RemoteAddressResolver. +Spring Cloud Gateway comes with one non-default remote address resolver which is based off of the X-Forwarded-For header, XForwardedRemoteAddressResolver. +XForwardedRemoteAddressResolver has two static constructor methods which take different approaches to security: +XForwardedRemoteAddressResolver::trustAll returns a RemoteAddressResolver which always takes the first IP address found in the X-Forwarded-For header. +This approach is vulnerable to spoofing, as a malicious client could set an initial value for the X-Forwarded-For which would be accepted by the resolver. +XForwardedRemoteAddressResolver::maxTrustedIndex takes an index which correlates to the number of trusted infrastructure running in front of Spring Cloud Gateway. +If Spring Cloud Gateway is, for example only accessible via HAProxy, then a value of 1 should be used. +If two hops of trusted infrastructure are required before Spring Cloud Gateway is accessible, then a value of 2 should be used. +Given the following header value: +X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3 +The maxTrustedIndex values below will yield the following remote addresses. + + + + + + +maxTrustedIndex +result + + + + +[Integer.MIN_VALUE,0] +(invalid, IllegalArgumentException during initialization) + + +1 +0.0.0.3 + + +2 +0.0.0.2 + + +3 +0.0.0.1 + + +[4, Integer.MAX_VALUE] +0.0.0.1 + + + + +Using Java config: +GatewayConfig.java +RemoteAddressResolver resolver = XForwardedRemoteAddressResolver + .maxTrustedIndex(1); + +... + +.route("direct-route", + r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24") + .uri("https://downstream1") +.route("proxied-route", + r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24") + .uri("https://downstream2") +) +
    +
    +
    + +GatewayFilter Factories +Route filters allow the modification of the incoming HTTP request or outgoing HTTP response in some manner. Route filters are scoped to a particular route. Spring Cloud Gateway includes many built-in GatewayFilter Factories. +NOTE For more detailed examples on how to use any of the following filters, take a look at the unit tests. +
    +AddRequestHeader GatewayFilter Factory +The AddRequestHeader GatewayFilter Factory takes a name and value parameter. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + filters: + - AddRequestHeader=X-Request-Foo, Bar + + +This will add X-Request-Foo:Bar header to the downstream request’s headers for all matching requests. +AddRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + predicates: + - Path=/foo/{segment} + filters: + - AddRequestHeader=X-Request-Foo, Bar-{segment} + + +
    +
    +AddRequestParameter GatewayFilter Factory +The AddRequestParameter GatewayFilter Factory takes a name and value parameter. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + filters: + - AddRequestParameter=foo, bar + + +This will add foo=bar to the downstream request’s query string for all matching requests. +AddRequestParameter is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddRequestParameter=foo, bar-{segment} + + +
    +
    +AddResponseHeader GatewayFilter Factory +The AddResponseHeader GatewayFilter Factory takes a name and value parameter. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + filters: + - AddResponseHeader=X-Response-Foo, Bar + + +This will add X-Response-Foo:Bar header to the downstream response’s headers for all matching requests. +AddResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddResponseHeader=foo, bar-{segment} + + +
    +
    +DedupeResponseHeader GatewayFilter Factory +The DedupeResponseHeader GatewayFilter Factory takes a name parameter and an optional strategy parameter. name can contain a list of header names, space separated. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: dedupe_response_header_route + uri: https://example.org + filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin + + +This will remove duplicate values of Access-Control-Allow-Credentials and Access-Control-Allow-Origin response headers in cases when both the gateway CORS logic and the downstream add them. +The DedupeResponseHeader filter also accepts an optional strategy parameter. The accepted values are RETAIN_FIRST (default), RETAIN_LAST, and RETAIN_UNIQUE. +
    +
    +Hystrix GatewayFilter Factory +Hystrix is a library from Netflix that implements the circuit breaker pattern. +The Hystrix GatewayFilter allows you to introduce circuit breakers to your gateway routes, protecting your services from cascading failures and allowing you to provide fallback responses in the event of downstream failures. +To enable Hystrix GatewayFilters in your project, add a dependency on spring-cloud-starter-netflix-hystrix from Spring Cloud Netflix. +The Hystrix GatewayFilter Factory requires a single name parameter, which is the name of the HystrixCommand. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: hystrix_route + uri: https://example.org + filters: + - Hystrix=myCommandName + + +This wraps the remaining filters in a HystrixCommand with command name myCommandName. +The Hystrix filter can also accept an optional fallbackUri parameter. Currently, only forward: schemed URIs are supported. If the fallback is called, the request will be forwarded to the controller matched by the URI. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: hystrix_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingserviceendpoint + filters: + - name: Hystrix + args: + name: fallbackcmd + fallbackUri: forward:/incaseoffailureusethis + - RewritePath=/consumingserviceendpoint, /backingserviceendpoint + + +This will forward to the /incaseoffailureusethis URI when the Hystrix fallback is called. Note that this example also demonstrates (optional) Spring Cloud Netflix Ribbon load-balancing via the lb prefix on the destination URI. +The primary scenario is to use the fallbackUri to an internal controller or handler within the gateway app. +However, it is also possible to reroute the request to a controller or handler in an external application, like so: + +application.yml + +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: Hystrix + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback + + +In this example, there is no fallback endpoint or handler in the gateway application, however, there is one in another +app, registered under http://localhost:9994. +In case of the request being forwarded to fallback, the Hystrix Gateway filter also provides the Throwable that has +caused it. It’s added to the ServerWebExchange as the +ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR attribute that can be used when +handling the fallback within the gateway app. +For the external controller/ handler scenario, headers can be added with exception details. You can find more information +on it in the FallbackHeaders GatewayFilter Factory section. +Hystrix settings (such as timeouts) can be configured with global defaults or on a route by route basis using application properties as explained on the Hystrix wiki. +To set a 5 second timeout for the example route above, the following configuration would be used: + +application.yml + +hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000 + + +
    +
    +FallbackHeaders GatewayFilter Factory +The FallbackHeaders factory allows you to add Hystrix execution exception details in headers of a request forwarded to +a fallbackUri in an external application, like in the following scenario: + +application.yml + +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: Hystrix + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback + filters: + - name: FallbackHeaders + args: + executionExceptionTypeHeaderName: Test-Header + + +In this example, after an execution exception occurs while running the HystrixCommand, the request will be forwarde to +the fallback endpoint or handler in an app running on localhost:9994. The headers with the exception type, message +and -if available- root cause exception type and message will be added to that request by the FallbackHeaders filter. +The names of the headers can be overwritten in the config by setting the values of the arguments listed below, along with +their default values: + + +executionExceptionTypeHeaderName ("Execution-Exception-Type") + + +executionExceptionMessageHeaderName ("Execution-Exception-Message") + + +rootCauseExceptionTypeHeaderName ("Root-Cause-Exception-Type") + + +rootCauseExceptionMessageHeaderName ("Root-Cause-Exception-Message") + + +You can find more information on how Hystrix works with Gateway in the Hystrix GatewayFilter Factory section. +
    +
    +MapRequestHeader GatewayFilter Factory +The MapRequestHeader GatewayFilter Facstory takes 'fromHeader' and 'toHeader' parameters. It creates a new named header (toHeader) and the value is extracted out of an existing named header (fromHeader) from the incoming http request. If the input header does not exist then the filter has no impact. If the new named header already exists then it’s values will be augmented with the new values. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: map_request_header_route + uri: https://example.org + filters: + - MapRequestHeader=Bar, X-Request-Foo + + +This will add X-Request-Foo:<values> header to the downstream request’s with updated values from the incoming http request Bar header. +
    +
    +PrefixPath GatewayFilter Factory +The PrefixPath GatewayFilter Factory takes a single prefix parameter. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - PrefixPath=/mypath + + +This will prefix /mypath to the path of all matching requests. So a request to /hello, would be sent to /mypath/hello. +
    +
    +PreserveHostHeader GatewayFilter Factory +The PreserveHostHeader GatewayFilter Factory has no parameters. This filter, sets a request attribute that the routing filter will inspect to determine if the original host header should be sent, rather than the host header determined by the http client. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: preserve_host_route + uri: https://example.org + filters: + - PreserveHostHeader + + +
    +
    +RequestRateLimiter GatewayFilter Factory +The RequestRateLimiter GatewayFilter Factory is uses a RateLimiter implementation to determine if the current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by default) is returned. +This filter takes an optional keyResolver parameter and parameters specific to the rate limiter (see below). +keyResolver is a bean that implements the KeyResolver interface. In configuration, reference the bean by name using SpEL. #{@myKeyResolver} is a SpEL expression referencing a bean with the name myKeyResolver. + +KeyResolver.java + +public interface KeyResolver { + Mono<String> resolve(ServerWebExchange exchange); +} + + +The KeyResolver interface allows pluggable strategies to derive the key for limiting requests. In future milestones, there will be some KeyResolver implementations. +The default implementation of KeyResolver is the PrincipalNameKeyResolver which retrieves the Principal from the ServerWebExchange and calls Principal.getName(). +By default, if the KeyResolver does not find a key, requests will be denied. This behavior can be adjust with the spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key (true or false) and spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code properties. + +The RequestRateLimiter is not configurable via the "shortcut" notation. The example below is invalid + + +application.properties + +# INVALID SHORTCUT CONFIGURATION +spring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2, 2, #{@userkeyresolver} + + +
    +Redis RateLimiter +The redis implementation is based off of work done at Stripe. It requires the use of the spring-boot-starter-data-redis-reactive Spring Boot starter. +The algorithm used is the Token Bucket Algorithm. +The redis-rate-limiter.replenishRate is how many requests per second do you want a user to be allowed to do, without any dropped requests. This is the rate that the token bucket is filled. +The redis-rate-limiter.burstCapacity is the maximum number of requests a user is allowed to do in a single second. This is the number of tokens the token bucket can hold. Setting this value to zero will block all requests. +A steady rate is accomplished by setting the same value in replenishRate and burstCapacity. Temporary bursts can be allowed by setting burstCapacity higher than replenishRate. In this case, the rate limiter needs to be allowed some time between bursts (according to replenishRate), as 2 consecutive bursts will result in dropped requests (HTTP 429 - Too Many Requests). + +application.yml + +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + + + +Config.java + +@Bean +KeyResolver userKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); +} + + +This defines a request rate limit of 10 per user. A burst of 20 is allowed, but the next second only 10 requests will be available. The KeyResolver is a simple one that gets the user request parameter (note: this is not recommended for production). +A rate limiter can also be defined as a bean implementing the RateLimiter interface. In configuration, reference the bean by name using SpEL. #{@myRateLimiter} is a SpEL expression referencing a bean with the name myRateLimiter. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + rate-limiter: "#{@myRateLimiter}" + key-resolver: "#{@userKeyResolver}" + + +
    +
    +
    +RedirectTo GatewayFilter Factory +The RedirectTo GatewayFilter Factory takes a status and a url parameter. The status should be a 300 series redirect http code, such as 301. The url should be a valid url. This will be the value of the Location header. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - RedirectTo=302, https://acme.org + + +This will send a status 302 with a Location:https://acme.org header to perform a redirect. +
    +
    +RemoveRequestHeader GatewayFilter Factory +The RemoveRequestHeader GatewayFilter Factory takes a name parameter. It is the name of the header to be removed. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: removerequestheader_route + uri: https://example.org + filters: + - RemoveRequestHeader=X-Request-Foo + + +This will remove the X-Request-Foo header before it is sent downstream. +
    +
    +RemoveResponseHeader GatewayFilter Factory +The RemoveResponseHeader GatewayFilter Factory takes a name parameter. It is the name of the header to be removed. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: removeresponseheader_route + uri: https://example.org + filters: + - RemoveResponseHeader=X-Response-Foo + + +This will remove the X-Response-Foo header from the response before it is returned to the gateway client. +To remove any kind of sensitive header you should configure this filter for any routes that you may +want to do so. In addition you can configure this filter once using spring.cloud.gateway.default-filters +and have it applied to all routes. +
    +
    +RewritePath GatewayFilter Factory +The RewritePath GatewayFilter Factory takes a path regexp parameter and a replacement parameter. This uses Java regular expressions for a flexible way to rewrite the request path. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: rewritepath_route + uri: https://example.org + predicates: + - Path=/foo/** + filters: + - RewritePath=/foo(?<segment>/?.*), $\{segment} + + +For a request path of /foo/bar, this will set the path to /bar before making the downstream request. Notice the $\ which is replaced with $ because of the YAML spec. +
    +
    +RewriteLocationResponseHeader GatewayFilter Factory +The RewriteLocationResponseHeader GatewayFilter Factory modifies the value of Location response header, usually to get rid of backend specific details. It takes stripVersionMode, locationHeaderName, hostValue, and protocolsRegex parameters. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: rewritelocationresponseheader_route + uri: http://example.org + filters: + - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , + + +For example, for a request POST https://api.example.com/some/object/name, Location response header value https://object-service.prod.example.net/v2/some/object/id will be rewritten as https://api.example.com/some/object/id. +Parameter stripVersionMode has the following possible values: NEVER_STRIP, AS_IN_REQUEST (default), ALWAYS_STRIP. + + +NEVER_STRIP - Version will not be stripped, even if the original request path contains no version + + +AS_IN_REQUEST - Version will be stripped only if the original request path contains no version + + +ALWAYS_STRIP - Version will be stripped, even if the original request path contains version + + +Parameter hostValue, if provided, will be used to replace the host:port portion of the response Location header. If not provided, the value of the Host request header will be used. +Parameter protocolsRegex must be a valid regex String, against which the protocol name will be matched. If not matched, the filter will do nothing. Default is http|https|ftp|ftps. +
    +
    +RewriteResponseHeader GatewayFilter Factory +The RewriteResponseHeader GatewayFilter Factory takes name, regexp, and replacement parameters. It uses Java regular expressions for a flexible way to rewrite the response header value. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: rewriteresponseheader_route + uri: https://example.org + filters: + - RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=*** + + +For a header value of /42?user=ford&password=omg!what&flag=true, it will be set to /42?user=ford&password=***&flag=true after making the downstream request. Please use $\ to mean $ because of the YAML spec. +
    +
    +SaveSession GatewayFilter Factory +The SaveSession GatewayFilter Factory forces a WebSession::save operation before forwarding the call downstream. This is of particular use when +using something like Spring Session with a lazy data store and need to ensure the session state has been saved before making the forwarded call. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: save_session + uri: https://example.org + predicates: + - Path=/foo/** + filters: + - SaveSession + + +If you are integrating Spring Security with Spring Session, and want to ensure security details have been forwarded to the remote process, this is critical. +
    +
    +SecureHeaders GatewayFilter Factory +The SecureHeaders GatewayFilter Factory adds a number of headers to the response at the recommendation from this blog post. + +The following headers are added (along with default values): + +X-Xss-Protection:1; mode=block + + +Strict-Transport-Security:max-age=631138519 + + +X-Frame-Options:DENY + + +X-Content-Type-Options:nosniff + + +Referrer-Policy:no-referrer + + +Content-Security-Policy:default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' + + +X-Download-Options:noopen + + +X-Permitted-Cross-Domain-Policies:none + + +To change the default values set the appropriate property in the spring.cloud.gateway.filter.secure-headers namespace: + +Property to change: + +xss-protection-header + + +strict-transport-security + + +frame-options + + +content-type-options + + +referrer-policy + + +content-security-policy + + +download-options + + +permitted-cross-domain-policies + + +To disable the default values set the property spring.cloud.gateway.filter.secure-headers.disable with comma separated values. + +Need use lowercase and full name of secure headers. + + +The following values can use: + +x-xss-protection + + +strict-transport-security + + +x-frame-options + + +x-content-type-options + + +referrer-policy + + +content-security-policy + + +x-download-options + + +x-permitted-cross-domain-policies + + + +Example: +spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security + +
    +
    +SetPath GatewayFilter Factory +The SetPath GatewayFilter Factory takes a path template parameter. It offers a simple way to manipulate the request path by allowing templated segments of the path. This uses the uri templates from Spring Framework. Multiple matching segments are allowed. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: setpath_route + uri: https://example.org + predicates: + - Path=/foo/{segment} + filters: + - SetPath=/{segment} + + +For a request path of /foo/bar, this will set the path to /bar before making the downstream request. +
    +
    +SetRequestHeader GatewayFilter Factory +The SetRequestHeader GatewayFilter Factory takes name and value parameters. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: setrequestheader_route + uri: https://example.org + filters: + - SetRequestHeader=X-Request-Foo, Bar + + +This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a X-Request-Foo:1234, this would be replaced with X-Request-Foo:Bar, which is what the downstream service would receive. +SetRequestHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: setrequestheader_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - SetRequestHeader=foo, bar-{segment} + + +
    +
    +SetResponseHeader GatewayFilter Factory +The SetResponseHeader GatewayFilter Factory takes name and value parameters. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: setresponseheader_route + uri: https://example.org + filters: + - SetResponseHeader=X-Response-Foo, Bar + + +This GatewayFilter replaces all headers with the given name, rather than adding. So if the downstream server responded with a X-Response-Foo:1234, this would be replaced with X-Response-Foo:Bar, which is what the gateway client would receive. +SetResponseHeader is aware of URI variables used to match a path or host. URI variables may be used in the value and will be expanded at runtime. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: setresponseheader_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - SetResponseHeader=foo, bar-{segment} + + +
    +
    +SetStatus GatewayFilter Factory +The SetStatus GatewayFilter Factory takes a single status parameter. It must be a valid Spring HttpStatus. It may be the integer value 404 or the string representation of the enumeration NOT_FOUND. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: setstatusstring_route + uri: https://example.org + filters: + - SetStatus=BAD_REQUEST + - id: setstatusint_route + uri: https://example.org + filters: + - SetStatus=401 + + +In either case, the HTTP status of the response will be set to 401. +
    +
    +StripPrefix GatewayFilter Factory +The StripPrefix GatewayFilter Factory takes one paramter, parts. The parts parameter indicated the number of parts in the path to strip from the request before sending it downstream. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: nameRoot + uri: http://nameservice + predicates: + - Path=/name/** + filters: + - StripPrefix=2 + + +When a request is made through the gateway to /name/bar/foo the request made to nameservice will look like http://nameservice/foo. +
    +
    +Retry GatewayFilter Factory +The Retry GatewayFilter Factory support following set of parameters: + + +retries: the number of retries that should be attempted + + +statuses: the HTTP status codes that should be retried, represented using org.springframework.http.HttpStatus + + +methods: the HTTP methods that should be retried, represented using org.springframework.http.HttpMethod + + +series: the series of status codes to be retried, represented using org.springframework.http.HttpStatus.Series + + +exceptions: list of exceptions thrown that should be retried + + +backoff: configured exponential backoff for the retries. Retries are performed after a backoff interval of firstBackoff * (factor ^ n) where n is the iteration. +If maxBackoff is configured, the maximum backoff applied will be limited to maxBackoff. +If basedOnPreviousValue is true, backoff will be calculated using prevBackoff * factor. + + +The following defaults are configured for Retry filter if enabled: + + +retries — 3 times + + +series — 5XX series + + +methods — GET method + + +exceptions — IOException and TimeoutException + + +backoff — disabled + + + +application.yml + +spring: + cloud: + gateway: + routes: + - id: retry_test + uri: http://localhost:8080/flakey + predicates: + - Host=*.retry.com + filters: + - name: Retry + args: + retries: 3 + statuses: BAD_GATEWAY + methods: GET,POST + backoff: + firstBackoff: 10ms + maxBackoff: 50ms + factor: 2 + basedOnPreviousValue: false + + + +When using the retry filter with a forward: prefixed URL, the target endpoint should be written carefully so that in case of an error it does not do anything that could result in a response being sent to the client and committed. For example, if the target endpoint is an annotated controller, the target controller method should not return ResponseEntity with an error status code. Instead it should throw an Exception, or signal an error, e.g. via a Mono.error(ex) return value, which the retry filter can be configured to handle by retrying. + + +When using the retry filter with any HTTP method with a body, the body will be cached and the gateway will become memory constrained. The body is cached in a request attribute defined by ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR. The type of the object is a org.springframework.core.io.buffer.DataBuffer. + +
    +
    +RequestSize GatewayFilter Factory +The RequestSize GatewayFilter Factory can restrict a request from reaching the downstream service , when the request size is greater than the permissible limit. The filter takes a maxSize parameter which is the permissible size limit of the request. The maxSize is a `DataSize type, so values can be defined as a number followed by an optional DataUnit suffix such as 'KB' or 'MB'. The default is 'B' for bytes. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: request_size_route + uri: http://localhost:8080/upload + predicates: + - Path=/upload + filters: + - name: RequestSize + args: + maxSize: 5000000 + + +The RequestSize GatewayFilter Factory set the response status as 413 Payload Too Large with a additional header errorMessage when the Request is rejected due to size. Following is an example of such an errorMessage . +errorMessage : Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB + +The default Request size will be set to 5 MB if not provided as filter argument in route definition. + +
    +
    +Modify Request Body GatewayFilter Factory +This filter is considered BETA and the API may change in the future +The ModifyRequestBody filter can be used to modify the request body before it is sent downstream by the Gateway. + +This filter can only be configured using the Java DSL + +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org") + .filters(f -> f.prefixPath("/httpbin") + .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, + (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri)) + .build(); +} + +static class Hello { + String message; + + public Hello() { } + + public Hello(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} +
    +
    +Modify Response Body GatewayFilter Factory +This filter is considered BETA and the API may change in the future +The ModifyResponseBody filter can be used to modify the response body before it is sent back to the Client. + +This filter can only be configured using the Java DSL + +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org") + .filters(f -> f.prefixPath("/httpbin") + .modifyResponseBody(String.class, String.class, + (exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri) + .build(); +} +
    +
    +Default Filters +If you would like to add a filter and apply it to all routes you can use spring.cloud.gateway.default-filters. +This property takes a list of filters + +application.yml + +spring: + cloud: + gateway: + default-filters: + - AddResponseHeader=X-Response-Default-Foo, Default-Bar + - PrefixPath=/httpbin + + +
    +
    + +Global Filters +The GlobalFilter interface has the same signature as GatewayFilter. These are special filters that are conditionally applied to all routes. (This interface and usage are subject to change in future milestones). +
    +Combined Global Filter and GatewayFilter Ordering +When a request comes in (and matches a Route) the Filtering Web Handler will add all instances of GlobalFilter and all route specific instances of GatewayFilter to a filter chain. This combined filter chain is sorted by the org.springframework.core.Ordered interface, which can be set by implementing the getOrder() method. +As Spring Cloud Gateway distinguishes between "pre" and "post" phases for filter logic execution (see: How it Works), the filter with the highest precedence will be the first in the "pre"-phase and the last in the "post"-phase. + +ExampleConfiguration.java + +@Bean +public GlobalFilter customFilter() { + return new CustomGlobalFilter(); +} + +public class CustomGlobalFilter implements GlobalFilter, Ordered { + + @Override + public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { + log.info("custom global filter"); + return chain.filter(exchange); + } + + @Override + public int getOrder() { + return -1; + } +} + + +
    +
    +Forward Routing Filter +The ForwardRoutingFilter looks for a URI in the exchange attribute ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a forward scheme (ie forward:///localendpoint), it will use the Spring DispatcherHandler to handler the request. The path part of the request URL will be overridden with the path in the forward URL. The unmodified original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute. +
    +
    +LoadBalancerClient Filter +The LoadBalancerClientFilter looks for a URI in the exchange attribute ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a lb scheme (ie lb://myservice), it will use the Spring Cloud LoadBalancerClient to resolve the name (myservice in the previous example) to an actual host and port and replace the URI in the same attribute. The unmodified original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute. The filter will also look in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR attribute to see if it equals lb and then the same rules apply. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: myRoute + uri: lb://service + predicates: + - Path=/service/** + + + +By default when a service instance cannot be found in the LoadBalancer a 503 will be returned. +You can configure the Gateway to return a 404 by setting spring.cloud.gateway.loadbalancer.use404=true. + + +The isSecure value of the ServiceInstance returned from the LoadBalancer will override +the scheme specified in the request made to the Gateway. For example, if the request comes into the Gateway over HTTPS +but the ServiceInstance indicates it is not secure, then the downstream request will be made over +HTTP. The opposite situation can also apply. However if GATEWAY_SCHEME_PREFIX_ATTR is specified for the +route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the +route URL will override the ServiceInstance configuration. + + +LoadBalancerClientFilter uses a blocking Ribbon LoadBalancerClient under the hood. +We suggest you use ReactiveLoadBalancerClientFilter instead. +You can switch to using it by adding org.springframework.cloud:spring-cloud-loadbalancer dependency to your project +and setting the value of the spring.cloud.loadbalancer.ribbon.enabled to false. + +
    +
    +ReactiveLoadBalancerClientFilter +The ReactiveLoadBalancerClientFilter looks for a URI in the exchange attribute +ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR. If the url has a lb scheme (ie lb://myservice), +it will use the Spring Cloud ReactorLoadBalancer to resolve the name (myservice in the previous example) +to an actual host and port and replace the URI in the same attribute. The unmodified +original url is appended to the list in the ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR attribute. +The filter will also look in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR attribute to see if it equals +lb and then the same rules apply. + +application.yml + +spring: + cloud: + gateway: + routes: + - id: myRoute + uri: lb://service + predicates: + - Path=/service/** + + + +By default when a service instance cannot be found by the ReactorLoadBalancer, a 503 will be returned. +You can configure the Gateway to return a 404 by setting spring.cloud.gateway.loadbalancer.use404=true. + + +The isSecure value of the ServiceInstance returned from the ReactiveLoadBalancerClientFilter will override +the scheme specified in the request made to the Gateway. For example, if the request comes into the Gateway over HTTPS +but the ServiceInstance indicates it is not secure, then the downstream request will be made over +HTTP. The opposite situation can also apply. However if GATEWAY_SCHEME_PREFIX_ATTR is specified for the +route in the Gateway configuration, the prefix will be stripped and the resulting scheme from the +route URL will override the ServiceInstance configuration. + +
    +
    +Netty Routing Filter +The Netty Routing Filter runs if the url located in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute has a http or https scheme. It uses the Netty HttpClient to make the downstream proxy request. The response is put in the ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR exchange attribute for use in a later filter. (There is an experimental WebClientHttpRoutingFilter that performs the same function, but does not require netty) +
    +
    +Netty Write Response Filter +The NettyWriteResponseFilter runs if there is a Netty HttpClientResponse in the ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR exchange attribute. It is run after all other filters have completed and writes the proxy response back to the gateway client response. (There is an experimental WebClientWriteResponseFilter that performs the same function, but does not require netty) +
    +
    +RouteToRequestUrl Filter +The RouteToRequestUrlFilter runs if there is a Route object in the ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR exchange attribute. It creates a new URI, based off of the request URI, but updated with the URI attribute of the Route object. The new URI is placed in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute. +If the URI has a scheme prefix, such as lb:ws://serviceid, the lb scheme is stripped from the URI and placed in the ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR for use later in the filter chain. +
    +
    +Websocket Routing Filter +The Websocket Routing Filter runs if the url located in the ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR exchange attribute has a ws or wss scheme. It uses the Spring Web Socket infrastructure to forward the Websocket request downstream. +Websockets may be load-balanced by prefixing the URI with lb, such as lb:ws://serviceid. + +If you are using SockJS as a fallback over normal http, you should configure a normal HTTP route as well as the Websocket Route. + + +application.yml + +spring: + cloud: + gateway: + routes: + # SockJS route + - id: websocket_sockjs_route + uri: http://localhost:3001 + predicates: + - Path=/websocket/info/** + # Normal Websocket route + - id: websocket_route + uri: ws://localhost:3001 + predicates: + - Path=/websocket/** + + +
    +
    +Gateway Metrics Filter +To enable Gateway Metrics add spring-boot-starter-actuator as a project dependency. Then, by default, the Gateway Metrics Filter runs as long as the property spring.cloud.gateway.metrics.enabled is not set to false. This filter adds a timer metric named "gateway.requests" with the following tags: + + +routeId: The route id + + +routeUri: The URI that the API will be routed to + + +outcome: Outcome as classified by HttpStatus.Series + + +status: Http Status of the request returned to the client + + +httpStatusCode: Http Status of the request returned to the client + + +httpMethod: The Http method used for the request + + +These metrics are then available to be scraped from /actuator/metrics/gateway.requests and can be easily integrated with Prometheus to create a Grafana dashboard. + +To enable the prometheus endpoint add micrometer-registry-prometheus as a project dependency. + +
    +
    +Marking An Exchange As Routed +After the Gateway has routed a ServerWebExchange it will mark that exchange as "routed" by adding gatewayAlreadyRouted +to the exchange attributes. Once a request has been marked as routed, other routing filters will not route the request again, +essentially skipping the filter. There are convenience methods that you can use to mark an exchange as routed +or check if an exchange has already been routed. + + +ServerWebExchangeUtils.isAlreadyRouted takes a ServerWebExchange object and checks if it has been "routed" + + +ServerWebExchangeUtils.setAlreadyRouted takes a ServerWebExchange object and marks it as "routed" + + +
    +
    + +HttpHeadersFilters +HttpHeadersFilters are applied to requests before sending them downstream, such as in the NettyRoutingFilter. +
    +Forwarded Headers Filter +The Forwarded Headers Filter creates a Forwarded header to send to the downstream service. It adds the Host header, scheme and port of the current request to any existing Forwarded header. +
    +
    +RemoveHopByHop Headers Filter +The RemoveHopByHop Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the IETF. + +The default removed headers are: + +Connection + + +Keep-Alive + + +Proxy-Authenticate + + +Proxy-Authorization + + +TE + + +Trailer + + +Transfer-Encoding + + +Upgrade + + +To change this, set the spring.cloud.gateway.filter.remove-non-proxy-headers.headers property to the list of header names to remove. +
    +
    +XForwarded Headers Filter +The XForwarded Headers Filter creates various a X-Forwarded-* headers to send to the downstream service. It users the Host header, scheme, port and path of the current request to create the various headers. +Creating of individual headers can be controlled by the following boolean properties (defaults to true): + + +spring.cloud.gateway.x-forwarded.for.enabled + + +spring.cloud.gateway.x-forwarded.host.enabled + + +spring.cloud.gateway.x-forwarded.port.enabled + + +spring.cloud.gateway.x-forwarded.proto.enabled + + +spring.cloud.gateway.x-forwarded.prefix.enabled + + +Appending multiple headers can be controlled by the following boolean properties (defaults to true): + + +spring.cloud.gateway.x-forwarded.for.append + + +spring.cloud.gateway.x-forwarded.host.append + + +spring.cloud.gateway.x-forwarded.port.append + + +spring.cloud.gateway.x-forwarded.proto.append + + +spring.cloud.gateway.x-forwarded.prefix.append + + +
    +
    + +TLS / SSL +The Gateway can listen for requests on https by following the usual Spring server configuration. Example: + +application.yml + +server: + ssl: + enabled: true + key-alias: scg + key-store-password: scg1234 + key-store: classpath:scg-keystore.p12 + key-store-type: PKCS12 + + +Gateway routes can be routed to both http and https backends. If routing to a https backend then the Gateway can be configured to trust all downstream certificates with the following configuration: + +application.yml + +spring: + cloud: + gateway: + httpclient: + ssl: + useInsecureTrustManager: true + + +Using an insecure trust manager is not suitable for production. For a production deployment the Gateway can be configured with a set of known certificates that it can trust with the follwing configuration: + +application.yml + +spring: + cloud: + gateway: + httpclient: + ssl: + trustedX509Certificates: + - cert1.pem + - cert2.pem + + +If the Spring Cloud Gateway is not provisioned with trusted certificates the default trust store is used (which can be overriden with system property javax.net.ssl.trustStore). +
    +TLS Handshake +The Gateway maintains a client pool that it uses to route to backends. When communicating over https the client initiates a TLS handshake. A number of timeouts are assoicated with this handshake. These timeouts can be configured (defaults shown): + +application.yml + +spring: + cloud: + gateway: + httpclient: + ssl: + handshake-timeout-millis: 10000 + close-notify-flush-timeout-millis: 3000 + close-notify-read-timeout-millis: 0 + + +
    +
    + +Configuration +Configuration for Spring Cloud Gateway is driven by a collection of RouteDefinitionLocators. + +RouteDefinitionLocator.java + +public interface RouteDefinitionLocator { + Flux<RouteDefinition> getRouteDefinitions(); +} + + +By default, a PropertiesRouteDefinitionLocator loads properties using Spring Boot’s @ConfigurationProperties mechanism. +The configuration examples above all use a shortcut notation that uses positional arguments rather than named ones. The two examples below are equivalent: + +application.yml + +spring: + cloud: + gateway: + routes: + - id: setstatus_route + uri: https://example.org + filters: + - name: SetStatus + args: + status: 401 + - id: setstatusshortcut_route + uri: https://example.org + filters: + - SetStatus=401 + + +For some usages of the gateway, properties will be adequate, but some production use cases will benefit from loading configuration from an external source, such as a database. Future milestone versions will have RouteDefinitionLocator implementations based off of Spring Data Repositories such as: Redis, MongoDB and Cassandra. +
    +Fluent Java Routes API +To allow for simple configuration in Java, there is a fluent API defined in the RouteLocatorBuilder bean. + +GatewaySampleApplication.java + +// static imports from GatewayFilters and RoutePredicates +@Bean +public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) { + return builder.routes() + .route(r -> r.host("**.abc.org").and().path("/image/png") + .filters(f -> + f.addResponseHeader("X-TestHeader", "foobar")) + .uri("http://httpbin.org:80") + ) + .route(r -> r.path("/image/webp") + .filters(f -> + f.addResponseHeader("X-AnotherHeader", "baz")) + .uri("http://httpbin.org:80") + ) + .route(r -> r.order(-1) + .host("**.throttle.org").and().path("/get") + .filters(f -> f.filter(throttle.apply(1, + 1, + 10, + TimeUnit.SECONDS))) + .uri("http://httpbin.org:80") + ) + .build(); +} + + +This style also allows for more custom predicate assertions. The predicates defined by RouteDefinitionLocator beans are combined using logical and. By using the fluent Java API, you can use the and(), or() and negate() operators on the Predicate class. +
    +
    +DiscoveryClient Route Definition Locator +The Gateway can be configured to create routes based on services registered with a DiscoveryClient compatible service registry. +To enable this, set spring.cloud.gateway.discovery.locator.enabled=true and make sure a DiscoveryClient implementation is on the classpath and enabled (such as Netflix Eureka, Consul or Zookeeper). +
    +Configuring Predicates and Filters For DiscoveryClient Routes +By default the Gateway defines a single predicate and filter for routes created via a DiscoveryClient. +The default predicate is a path predicate defined with the pattern /serviceId/**, where serviceId is +the id of the service from the DiscoveryClient. +The default filter is rewrite path filter with the regex /serviceId/(?<remaining>.*) and the replacement +/${remaining}. This just strips the service id from the path before the request is sent +downstream. +If you would like to customize the predicates and/or filters used by the DiscoveryClient routes you can do so +by setting spring.cloud.gateway.discovery.locator.predicates[x] and spring.cloud.gateway.discovery.locator.filters[y]. +When doing so you need to make sure to include the default predicate and filter above, if you want to retain +that functionality. Below is an example of what this looks like. + +application.properties + +spring.cloud.gateway.discovery.locator.predicates[0].name: Path +spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'" +spring.cloud.gateway.discovery.locator.predicates[1].name: Host +spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'" +spring.cloud.gateway.discovery.locator.filters[0].name: Hystrix +spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId +spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath +spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/(?<remaining>.*)'" +spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'" + + +
    +
    +
    + +Reactor Netty Access Logs +To enable Reactor Netty access logs, set -Dreactor.netty.http.server.accessLogEnabled=true. (It must be a Java System Property, not a Spring Boot property). +The logging system can be configured to have a separate access log file. Below is an example logback configuration: + +logback.xml + + <appender name="accessLog" class="ch.qos.logback.core.FileAppender"> + <file>access_log.log</file> + <encoder> + <pattern>%msg%n</pattern> + </encoder> + </appender> + <appender name="async" class="ch.qos.logback.classic.AsyncAppender"> + <appender-ref ref="accessLog" /> + </appender> + + <logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false"> + <appender-ref ref="async"/> + </logger> + + + + +CORS Configuration +The gateway can be configured to control CORS behavior. The "global" CORS configuration is a map of URL patterns to Spring Framework CorsConfiguration. + +application.yml + +spring: + cloud: + gateway: + globalcors: + corsConfigurations: + '[/**]': + allowedOrigins: "https://docs.spring.io" + allowedMethods: + - GET + + +In the example above, CORS requests will be allowed from requests that originate from docs.spring.io for all GET requested paths. +To provide the same CORS configuration to requests that are not handled by some gateway route predicate, set the property spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping equal to true. This is useful when trying to support CORS preflight requests and your route predicate doesn’t evalute to true because the http method is options. + + +Actuator API +The /gateway actuator endpoint allows to monitor and interact with a Spring Cloud Gateway application. To be remotely accessible, the endpoint has to be enabled and exposed via HTTP or JMX in the application properties. + +application.properties + +management.endpoint.gateway.enabled=true # default value +management.endpoints.web.exposure.include=gateway + + +
    +Verbose Actuator Format +A new, more verbose format has been added to Gateway. This adds more detail to each route allowing to view the predicates and filters associated to each route along with any configuration that is available. +/actuator/gateway/routes +[ + { + "predicate": "(Hosts: [**.addrequestheader.org] && Paths: [/headers], match trailing slash: true)", + "route_id": "add_request_header_test", + "filters": [ + "[[AddResponseHeader X-Response-Default-Foo = 'Default-Bar'], order = 1]", + "[[AddRequestHeader X-Request-Foo = 'Bar'], order = 1]", + "[[PrefixPath prefix = '/httpbin'], order = 2]" + ], + "uri": "lb://testservice", + "order": 0 + } +] +To enable this feature, set the following property: + +application.properties + +spring.cloud.gateway.actuator.verbose.enabled=true + + +This will default to true in a future release. +
    +
    +Retrieving route filters +
    +Global Filters +To retrieve the global filters applied to all routes, make a GET request to /actuator/gateway/globalfilters. The resulting response is similar to the following: +{ + "org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5": 10100, + "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4f6fd101": 10000, + "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@32d22650": -1, + "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@106459d9": 2147483647, + "org.springframework.cloud.gateway.filter.NettyRoutingFilter@1fbd5e0": 2147483647, + "org.springframework.cloud.gateway.filter.ForwardPathFilter@33a71d23": 0, + "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@135064ea": 2147483637, + "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@23c05889": 2147483646 +} +The response contains details of the global filters in place. For each global filter is provided the string representation of the filter object (e.g., org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5) and the corresponding order in the filter chain. +
    +
    +Route Filters +To retrieve the GatewayFilter factories applied to routes, make a GET request to /actuator/gateway/routefilters. The resulting response is similar to the following: +{ + "[AddRequestHeaderGatewayFilterFactory@570ed9c configClass = AbstractNameValueGatewayFilterFactory.NameValueConfig]": null, + "[SecureHeadersGatewayFilterFactory@fceab5d configClass = Object]": null, + "[SaveSessionGatewayFilterFactory@4449b273 configClass = Object]": null +} +The response contains details of the GatewayFilter factories applied to any particular route. For each factory is provided the string representation of the corresponding object (e.g., [SecureHeadersGatewayFilterFactory@fceab5d configClass = Object]). Note that the null value is due to an incomplete implementation of the endpoint controller, for that it tries to set the order of the object in the filter chain, which does not apply to a GatewayFilter factory object. +
    +
    +
    +Refreshing the route cache +To clear the routes cache, make a POST request to /actuator/gateway/refresh. The request returns a 200 without response body. +
    +
    +Retrieving the routes defined in the gateway +To retrieve the routes defined in the gateway, make a GET request to /actuator/gateway/routes. The resulting response is similar to the following: +[{ + "route_id": "first_route", + "route_object": { + "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@1e9d7e7d", + "filters": [ + "OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory$$Lambda$436/674480275@6631ef72, order=0}" + ] + }, + "order": 0 +}, +{ + "route_id": "second_route", + "route_object": { + "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/1736826640@cd8d298", + "filters": [] + }, + "order": 0 +}] +The response contains details of all the routes defined in the gateway. The following table describes the structure of each element (i.e., a route) of the response. + + + + + + + +Path +Type +Description + + + + +route_id +String +The route id. + + +route_object.predicate +Object +The route predicate. + + +route_object.filters +Array +The GatewayFilter factories applied to the route. + + +order +Number +The route order. + + + + +
    +
    +Retrieving information about a particular route +To retrieve information about a single route, make a GET request to /actuator/gateway/routes/{id} (e.g., /actuator/gateway/routes/first_route). The resulting response is similar to the following: +{ + "id": "first_route", + "predicates": [{ + "name": "Path", + "args": {"_genkey_0":"/first"} + }], + "filters": [], + "uri": "https://www.uri-destination.org", + "order": 0 +}] +The following table describes the structure of the response. + + + + + + + +Path +Type +Description + + + + +id +String +The route id. + + +predicates +Array +The collection of route predicates. Each item defines the name and the arguments of a given predicate. + + +filters +Array +The collection of filters applied to the route. + + +uri +String +The destination URI of the route. + + +order +Number +The route order. + + + + +
    +
    +Creating and deleting a particular route +To create a route, make a POST request to /gateway/routes/{id_route_to_create} with a JSON body that specifies the fields of the route (see the previous subsection). +To delete a route, make a DELETE request to /gateway/routes/{id_route_to_delete}. +
    +
    +Recap: list of all endpoints +The table below summarises the Spring Cloud Gateway actuator endpoints. Note that each endpoint has /actuator/gateway as the base-path. + + + + + + + +ID +HTTP Method +Description + + + + +globalfilters +GET +Displays the list of global filters applied to the routes. + + +routefilters +GET +Displays the list of GatewayFilter factories applied to a particular route. + + +refresh +POST +Clears the routes cache. + + +routes +GET +Displays the list of routes defined in the gateway. + + +routes/{id} +GET +Displays information about a particular route. + + +routes/{id} +POST +Add a new route to the gateway. + + +routes/{id} +DELETE +Remove an existing route from the gateway. + + + + +
    +
    + +Troubleshooting +
    +Log Levels +Below are some useful loggers that contain valuable trouble shooting infomration at the DEBUG and TRACE levels. + + +org.springframework.cloud.gateway + + +org.springframework.http.server.reactive + + +org.springframework.web.reactive + + +org.springframework.boot.autoconfigure.web + + +reactor.netty + + +redisratelimiter + + +
    +
    +Wiretap +The Reactor Netty HttpClient and HttpServer can have wiretap enabled. When combined +with setting the reactor.netty log level to DEBUG or TRACE will enable logging of +information such as headers and bodies sent and received across the wire. To enable this, +set spring.cloud.gateway.httpserver.wiretap=true and/or +spring.cloud.gateway.httpclient.wiretap=true for the HttpServer and HttpClient +respectively. +
    +
    + +Developer Guide +These are basic guides to writing some custom components of the gateway. +
    +Writing Custom Route Predicate Factories +In order to write a Route Predicate you will need to implement RoutePredicateFactory. There is an abstract class called AbstractRoutePredicateFactory which you can extend. + +MyRoutePredicateFactory.java + +public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> { + + public MyRoutePredicateFactory() { + super(Config.class); + } + + @Override + public Predicate<ServerWebExchange> apply(Config config) { + // grab configuration from Config object + return exchange -> { + //grab the request + ServerHttpRequest request = exchange.getRequest(); + //take information from the request to see if it + //matches configuration. + return matches(config, request); + }; + } + + public static class Config { + //Put the configuration properties for your filter here + } + +} + + +
    +
    +Writing Custom GatewayFilter Factories +In order to write a GatewayFilter you will need to implement GatewayFilterFactory. There is an abstract class called AbstractGatewayFilterFactory which you can extend. + +PreGatewayFilterFactory.java + +public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory<PreGatewayFilterFactory.Config> { + + public PreGatewayFilterFactory() { + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + // grab configuration from Config object + return (exchange, chain) -> { + //If you want to build a "pre" filter you need to manipulate the + //request before calling chain.filter + ServerHttpRequest.Builder builder = exchange.getRequest().mutate(); + //use builder to manipulate the request + return chain.filter(exchange.mutate().request(request).build()); + }; + } + + public static class Config { + //Put the configuration properties for your filter here + } + +} + + + +PostGatewayFilterFactory.java + +public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory<PostGatewayFilterFactory.Config> { + + public PostGatewayFilterFactory() { + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + // grab configuration from Config object + return (exchange, chain) -> { + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + ServerHttpResponse response = exchange.getResponse(); + //Manipulate the response in some way + })); + }; + } + + public static class Config { + //Put the configuration properties for your filter here + } + +} + + +
    +
    +Writing Custom Global Filters +In order to write a custom global filter, you will need to implement GlobalFilter interface. This will apply the filter to all requests. +Example of how to set up a Global Pre and Post filter, respectively +@Bean +public GlobalFilter customGlobalFilter() { + return (exchange, chain) -> exchange.getPrincipal() + .map(Principal::getName) + .defaultIfEmpty("Default User") + .map(userName -> { + //adds header to proxied request + exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build(); + return exchange; + }) + .flatMap(chain::filter); +} + +@Bean +public GlobalFilter customGlobalPostFilter() { + return (exchange, chain) -> chain.filter(exchange) + .then(Mono.just(exchange)) + .map(serverWebExchange -> { + //adds header to response + serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER", + HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked": "It did not work"); + return serverWebExchange; + }) + .then(); +} +
    +
    + +Building a Simple Gateway Using Spring MVC or Webflux + +The following describes an alternative style gateway. None of the prior documentation applies to what follows. + +Spring Cloud Gateway provides a utility object called ProxyExchange which you can use inside a regular Spring web handler as a method parameter. It supports basic downstream HTTP exchanges via methods that mirror the HTTP verbs. With MVC it also supports forwarding to a local handler via the forward() method. To use the ProxyExchange just include the right module in your classpath (either spring-cloud-gateway-mvc or spring-cloud-gateway-webflux). +MVC example (proxying a request to "/test" downstream to a remote server): +@RestController +@SpringBootApplication +public class GatewaySampleApplication { + + @Value("${remote.home}") + private URI home; + + @GetMapping("/test") + public ResponseEntity<?> proxy(ProxyExchange<byte[]> proxy) throws Exception { + return proxy.uri(home.toString() + "/image/png").get(); + } + +} +The same thing with Webflux: +@RestController +@SpringBootApplication +public class GatewaySampleApplication { + + @Value("${remote.home}") + private URI home; + + @GetMapping("/test") + public Mono<ResponseEntity<?>> proxy(ProxyExchange<byte[]> proxy) throws Exception { + return proxy.uri(home.toString() + "/image/png").get(); + } + +} +There are convenience methods on the ProxyExchange to enable the handler method to discover and enhance the URI path of the incoming request. For example you might want to extract the trailing elements of a path to pass them downstream: +@GetMapping("/proxy/path/**") +public ResponseEntity<?> proxyPath(ProxyExchange<byte[]> proxy) throws Exception { + String path = proxy.path("/proxy/path/"); + return proxy.uri(home.toString() + "/foos/" + path).get(); +} +All the features of Spring MVC or Webflux are available to Gateway handler methods. So you can inject request headers and query parameters, for instance, and you can constrain the incoming requests with declarations in the mapping annotation. See the documentation for @RequestMapping in Spring MVC for more details of those features. +Headers can be added to the downstream response using the header() methods on ProxyExchange. +You can also manipulate response headers (and anything else you like in the response) by adding a mapper to the get() etc. method. The mapper is a Function that takes the incoming ResponseEntity and converts it to an outgoing one. +First class support is provided for "sensitive" headers ("cookie" and "authorization" by default) which are not passed downstream, and for "proxy" headers (x-forwarded-*). + +
    + +Spring Cloud Function + +Mark Fisher, Dave Syer, Oleg Zhurakousky + + + +Introduction +Spring Cloud Function is a project with the following high-level goals: + + +Promote the implementation of business logic via functions. + + +Decouple the development lifecycle of business logic from any specific runtime target so that the same code can run as a web endpoint, a stream processor, or a task. + + +Support a uniform programming model across serverless providers, as well as the ability to run standalone (locally or in a PaaS). + + +Enable Spring Boot features (auto-configuration, dependency injection, metrics) on serverless providers. + + +It abstracts away all of the transport details and +infrastructure, allowing the developer to keep all the familiar tools +and processes, and focus firmly on business logic. +Here’s a complete, executable, testable Spring Boot application +(implementing a simple string manipulation): +@SpringBootApplication +public class Application { + + @Bean + public Function<Flux<String>, Flux<String>> uppercase() { + return flux -> flux.map(value -> value.toUpperCase()); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +It’s just a Spring Boot application, so it can be built, run and +tested, locally and in a CI build, the same way as any other Spring +Boot application. The Function is from java.util and Flux is a +Reactive Streams Publisher from +Project Reactor. The function can be +accessed over HTTP or messaging. +Spring Cloud Function has 4 main features: + + +Wrappers for @Beans of type Function, Consumer and +Supplier, exposing them to the outside world as either HTTP +endpoints and/or message stream listeners/publishers with RabbitMQ, Kafka etc. + + +Compiling strings which are Java function bodies into bytecode, and +then turning them into @Beans that can be wrapped as above. + + +Deploying a JAR file containing such an application context with an +isolated classloader, so that you can pack them together in a single +JVM. + + +Adapters for AWS Lambda, Azure, Apache OpenWhisk and possibly other "serverless" service providers. + + + +Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at github. + + + +Getting Started +Build from the command line (and "install" the samples): +$ ./mvnw clean install +(If you like to YOLO add -DskipTests.) +Run one of the samples, e.g. +$ java -jar spring-cloud-function-samples/function-sample/target/*.jar +This runs the app and exposes its functions over HTTP, so you can +convert a string to uppercase, like this: +$ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d Hello +HELLO +You can convert multiple strings (a Flux<String>) by separating them +with new lines +$ curl -H "Content-Type: text/plain" localhost:8080/uppercase -d 'Hello +> World' +HELLOWORLD +(You can use QJ in a terminal to insert a new line in a literal +string like that.) + + +Building and Running a Function +The sample @SpringBootApplication above has a function that can be +decorated at runtime by Spring Cloud Function to be an HTTP endpoint, +or a Stream processor, for instance with RabbitMQ, Apache Kafka or +JMS. +The @Beans can be Function, Consumer or Supplier (all from +java.util), and their parametric types can be String or POJO. +Functions can also be of Flux<String> or Flux<Pojo> and Spring +Cloud Function takes care of converting the data to and from the +desired types, as long as it comes in as plain text or (in the case of +the POJO) JSON. There is also support for Message<Pojo> where the +message headers are copied from the incoming event, depending on the +adapter. The web adapter also supports conversion from form-encoded +data to a Map, and if you are using the function with Spring Cloud +Stream then all the conversion and coercion features for message +payloads will be applicable as well. +Functions can be grouped together in a single application, or deployed +one-per-jar. It’s up to the developer to choose. An app with multiple +functions can be deployed multiple times in different "personalities", +exposing different functions over different physical transports. + + +Function Catalog and Flexible Function Signatures +One of the main features of Spring Cloud Function is to adapt and support a range of type signatures for user-defined functions, +while providing a consistent execution model. +That’s why all user defined functions are transformed into a canonical representation by FunctionCatalog, using primitives +defined by the Project Reactor (i.e., Flux<T> and Mono<T>). +Users can supply a bean of type Function<String,String>, for instance, and the FunctionCatalog will wrap it into a +Function<Flux<String>,Flux<String>>. +Using Reactor based primitives not only helps with the canonical representation of user defined functions, but it also +facilitates a more robust and flexible(reactive) execution model. +While users don’t normally have to care about the FunctionCatalog at all, it is useful to know what +kind of functions are supported in user code. +
    +Java 8 function support +Generally speaking users can expect that if they write a function for +a plain old Java type (or primitive wrapper), then the function +catalog will wrap it to a Flux of the same type. If the user writes +a function using Message (from spring-messaging) it will receive and +transmit headers from any adapter that supports key-value metadata +(e.g. HTTP headers). Here are the details. + + + + + + + +User Function +Catalog Registration + + + + + +Function<S,T> +Function<Flux<S>, Flux<T>> + + + +Function<Message<S>,Message<T>> +Function<Flux<Message<S>>, Flux<Message<T>>> + + + +Function<Flux<S>, Flux<T>> +Function<Flux<S>, Flux<T>> (pass through) + + + +Supplier<T> +Supplier<Flux<T>> + + + +Supplier<Flux<T>> +Supplier<Flux<T>> + + + +Consumer<T> +Function<Flux<T>, Mono<Void>> + + + +Consumer<Message<T>> +Function<Flux<Message<T>>, Mono<Void>> + + + +Consumer<Flux<T>> +Consumer<Flux<T>> + + + + + +Consumer is a little bit special because it has a void return type, +which implies blocking, at least potentially. Most likely you will not +need to write Consumer<Flux<?>>, but if you do need to do that, +remember to subscribe to the input flux. If you declare a Consumer +of a non publisher type (which is normal), it will be converted to a +function that returns a publisher, so that it can be subscribed to in +a controlled way. +
    +
    +Kotlin Lambda support +We also provide support for Kotlin lambdas (since v2.0). +Consider the following: +@Bean +open fun kotlinSupplier(): () -> String { + return { "Hello from Kotlin" } +} + +@Bean +open fun kotlinFunction(): (String) -> String { + return { it.toUpperCase() } +} + +@Bean +open fun kotlinConsumer(): (String) -> Unit { + return { println(it) } +} +The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of +Supplier, Function and Consumer, and thus supported/recognized signatures by the framework. +While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the +same rules for signature transformation outlined in "Java 8 function support" section are applied here as well. +To enable Kotlin support all you need is to add spring-cloud-function-kotlin module to your classpath which contains the appropriate +autoconfiguration and supporting classes. +
    +
    + +Standalone Web Applications +The spring-cloud-function-web module has autoconfiguration that +activates when it is included in a Spring Boot web application (with +MVC support). There is also a spring-cloud-starter-function-web to +collect all the optional dependencies in case you just want a simple +getting started experience. +With the web configurations activated your app will have an MVC +endpoint (on "/" by default, but configurable with +spring.cloud.function.web.path) that can be used to access the +functions in the application context. The supported content types are +plain text and JSON. + + + + + + + + + +Method +Path +Request +Response +Status + + + + +GET +/{supplier} +- +Items from the named supplier +200 OK + + +POST +/{consumer} +JSON object or text +Mirrors input and pushes request body into consumer +202 Accepted + + +POST +/{consumer} +JSON array or text with new lines +Mirrors input and pushes body into consumer one by one +202 Accepted + + +POST +/{function} +JSON object or text +The result of applying the named function +200 OK + + +POST +/{function} +JSON array or text with new lines +The result of applying the named function +200 OK + + +GET +/{function}/{item} +- +Convert the item into an object and return the result of applying the function +200 OK + + + + +As the table above shows the behaviour of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or Flux), then the response will also contain a single value. +For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream". +If there is only a single function (consumer etc.) in the catalog, the name in the path is optional. +Composite functions can be addressed using pipes or commas to separate function names (pipes are legal in URL paths, but a bit awkward to type on the command line). +For cases where there is more then a single function in catalog and you want to map a specific function to the root +path (e.g., "/"), or you want to compose several functions and then map to the root path you can do so by providing +spring.cloud.function.definition property which essentially used by spring-=cloud-function-web module to provide +default mapping for cases where there is some type of a conflict (e.g., more then one function available etc). +For example, +--spring.cloud.function.definition=foo|bar +The above property will compose 'foo' and 'bar' function and map the composed function to the "/" path. +Functions and consumers that are declared with input and output in Message<?> will see the request headers on the input messages, and the output message headers will be converted to HTTP headers. +When POSTing text the response format might be different with Spring Boot 2.0 and older versions, depending on the content negotiation (provide content type and accpt headers for the best results). + + +Standalone Streaming Applications +To send or receive messages from a broker (such as RabbitMQ or Kafka) you can leverage spring-cloud-stream project and it’s integration with Spring Cloud Function. +Please refer to Spring Cloud Function section of the Spring Cloud Stream reference manual for more details and examples. + + +Deploying a Packaged Function +Spring Cloud Function provides a "deployer" library that allows you to launch a jar file (or exploded archive, or set of jar files) with an isolated class loader and expose the functions defined in it. This is quite a powerful tool that would allow you to, for instance, adapt a function to a range of different input-output adapters without changing the target jar file. Serverless platforms often have this kind of feature built in, so you could see it as a building block for a function invoker in such a platform (indeed the Riff Java function invoker uses this library). +The standard entry point of the API is the Spring configuration annotation @EnableFunctionDeployer. If that is used in a Spring Boot application the deployer kicks in and looks for some configuration to tell it where to find the function jar. At a minimum the user has to provide a function.location which is a URL or resource location for the archive containing the functions. It can optionally use a maven: prefix to locate the artifact via a dependency lookup (see FunctionProperties for complete details). A Spring Boot application is bootstrapped from the jar file, using the MANIFEST.MF to locate a start class, so that a standard Spring Boot fat jar works well, for example. If the target jar can be launched successfully then the result is a function registered in the main application’s FunctionCatalog. The registered function can be applied by code in the main application, even though it was created in an isolated class loader (by deault). + + +Functional Bean Definitions +Spring Cloud Function supports a "functional" style of bean declarations for small apps where you need fast startup. The functional style of bean declaration was a feature of Spring Framework 5.0 with significant enhancements in 5.1. +
    +Comparing Functional with Traditional Bean Definitions +Here’s a vanilla Spring Cloud Function application from with the +familiar @Configuration and @Bean declaration style: +@SpringBootApplication +public class DemoApplication { + + @Bean + public Function<String, String> uppercase() { + return value -> value.toUpperCase(); + } + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} +You can run the above in a serverless platform, like AWS Lambda or Azure Functions, or you can run it in its own HTTP server just by including spring-cloud-function-starter-web on the classpath. Running the main method would expose an endpoint that you can use to ping that uppercase function: +$ curl localhost:8080 -d foo +FOO +The web adapter in spring-cloud-function-starter-web uses Spring MVC, so you needed a Servlet container. You can also use Webflux where the default server is netty (even though you can still use Servlet containers if you want to) - just include the spring-cloud-starter-function-webflux dependency instead. The functionality is the same, and the user application code can be used in both. +Now for the functional beans: the user application code can be recast into "functional" +form, like this: +@SpringBootConfiguration +public class DemoApplication implements ApplicationContextInitializer<GenericApplicationContext> { + + public static void main(String[] args) { + FunctionalSpringApplication.run(DemoApplication.class, args); + } + + public Function<String, String> uppercase() { + return value -> value.toUpperCase(); + } + + @Override + public void initialize(GenericApplicationContext context) { + context.registerBean("demo", FunctionRegistration.class, + () -> new FunctionRegistration<>(uppercase()) + .type(FunctionType.from(String.class).to(String.class))); + } + +} +The main differences are: + + +The main class is an ApplicationContextInitializer. + + +The @Bean methods have been converted to calls to context.registerBean() + + +The @SpringBootApplication has been replaced with +@SpringBootConfiguration to signify that we are not enabling Spring +Boot autoconfiguration, and yet still marking the class as an "entry +point". + + +The SpringApplication from Spring Boot has been replaced with a +FunctionalSpringApplication from Spring Cloud Function (it’s a +subclass). + + +The business logic beans that you register in a Spring Cloud Function app are of type FunctionRegistration. This is a wrapper that contains both the function and information about the input and output types. In the @Bean form of the application that information can be derived reflectively, but in a functional bean registration some of it is lost unless we use a FunctionRegistration. +An alternative to using an ApplicationContextInitializer and FunctionRegistration is to make the application itself implement Function (or Consumer or Supplier). Example (equivalent to the above): +@SpringBootConfiguration +public class DemoApplication implements Function<String, String> { + + public static void main(String[] args) { + FunctionalSpringApplication.run(DemoApplication.class, args); + } + + @Override + public String uppercase(String value) { + return value.toUpperCase(); + } + +} +It would also work if you add a separate, standalone class of type Function and register it with the SpringApplication using an alternative form of the run() method. The main thing is that the generic type information is available at runtime through the class declaration. +The app runs in its own HTTP server if you add spring-cloud-starter-function-webflux (it won’t work with the MVC starter at the moment because the functional form of the embedded Servlet container hasn’t been implemented). The app also runs just fine in AWS Lambda or Azure Functions, and the improvements in startup time are dramatic. + +The "lite" web server has some limitations for the range of Function signatures - in particular it doesn’t (yet) support Message input and output, but POJOs and any kind of Publisher should be fine. + +
    +
    +Testing Functional Applications +Spring Cloud Function also has some utilities for integration testing that will be very familiar to Spring Boot users. For example, here is an integration test for the HTTP server wrapping the app above: +@RunWith(SpringRunner.class) +@FunctionalSpringBootTest +@AutoConfigureWebTestClient +public class FunctionalTests { + + @Autowired + private WebTestClient client; + + @Test + public void words() throws Exception { + client.post().uri("/").body(Mono.just("foo"), String.class).exchange() + .expectStatus().isOk().expectBody(String.class).isEqualTo("FOO"); + } + +} +This test is almost identical to the one you would write for the @Bean version of the same app - the only difference is the @FunctionalSpringBootTest annotation, instead of the regular @SpringBootTest. All the other pieces, like the @Autowired WebTestClient, are standard Spring Boot features. +Or you could write a test for a non-HTTP app using just the FunctionCatalog. For example: +@RunWith(SpringRunner.class) +@FunctionalSpringBootTest +public class FunctionalTests { + + @Autowired + private FunctionCatalog catalog; + + @Test + public void words() throws Exception { + Function<Flux<String>, Flux<String>> function = catalog.lookup(Function.class, + "function"); + assertThat(function.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO"); + } + +} +(The FunctionCatalog always returns functions from Flux to Flux, even if the user declares them with a simpler signature.) +
    +
    +Limitations of Functional Bean Declaration +Most Spring Cloud Function apps have a relatively small scope compared to the whole of Spring Boot, so we are able to adapt it to these functional bean definitions easily. If you step outside that limited scope, you can extend your Spring Cloud Function app by switching back to @Bean style configuration, or by using a hybrid approach. If you want to take advantage of Spring Boot autoconfiguration for integrations with external datastores, for example, you will need to use @EnableAutoConfiguration. Your functions can still be defined using the functional declarations if you want (i.e. the "hybrid" style), but in that case you will need to explicitly switch off the "full functional mode" using spring.functional.enabled=false so that Spring Boot can take back control. +
    +
    + +Dynamic Compilation +There is a sample app that uses the function compiler to create a +function from a configuration property. The vanilla "function-sample" +also has that feature. And there are some scripts that you can run to +see the compilation happening at run time. To run these examples, +change into the scripts directory: +cd scripts +Also, start a RabbitMQ server locally (e.g. execute rabbitmq-server). +Start the Function Registry Service: +./function-registry.sh +Register a Function: +./registerFunction.sh -n uppercase -f "f->f.map(s->s.toString().toUpperCase())" +Run a REST Microservice using that Function: +./web.sh -f uppercase -p 9000 +curl -H "Content-Type: text/plain" -H "Accept: text/plain" localhost:9000/uppercase -d foo +Register a Supplier: +./registerSupplier.sh -n words -f "()->Flux.just(\"foo\",\"bar\")" +Run a REST Microservice using that Supplier: +./web.sh -s words -p 9001 +curl -H "Accept: application/json" localhost:9001/words +Register a Consumer: +./registerConsumer.sh -n print -t String -f "System.out::println" +Run a REST Microservice using that Consumer: +./web.sh -c print -p 9002 +curl -X POST -H "Content-Type: text/plain" -d foo localhost:9002/print +Run Stream Processing Microservices: +First register a streaming words supplier: +./registerSupplier.sh -n wordstream -f "()->Flux.interval(Duration.ofMillis(1000)).map(i->\"message-\"+i)" +Then start the source (supplier), processor (function), and sink (consumer) apps +(in reverse order): +./stream.sh -p 9103 -i uppercaseWords -c print +./stream.sh -p 9102 -i words -f uppercase -o uppercaseWords +./stream.sh -p 9101 -s wordstream -o words +The output will appear in the console of the sink app (one message per second, converted to uppercase): +MESSAGE-0 +MESSAGE-1 +MESSAGE-2 +MESSAGE-3 +MESSAGE-4 +MESSAGE-5 +MESSAGE-6 +MESSAGE-7 +MESSAGE-8 +MESSAGE-9 +... + + +Serverless Platform Adapters +As well as being able to run as a standalone process, a Spring Cloud +Function application can be adapted to run one of the existing +serverless platforms. In the project there are adapters for +AWS +Lambda, +Azure, +and +Apache +OpenWhisk. The Oracle Fn platform +has its own Spring Cloud Function adapter. And +Riff supports Java functions and its +Java Function +Invoker acts natively is an adapter for Spring Cloud Function jars. +
    +AWS Lambda +The AWS adapter takes a Spring Cloud Function app and converts it to a form that can run in AWS Lambda. +
    +Introduction +The adapter has a couple of generic request handlers that you can use. The most generic is SpringBootStreamHandler, which uses a Jackson ObjectMapper provided by Spring Boot to serialize and deserialize the objects in the function. There is also a SpringBootRequestHandler which you can extend, and provide the input and output types as type parameters (enabling AWS to inspect the class and do the JSON conversions itself). +If your app has more than one @Bean of type Function etc. then you can choose the one to use by configuring function.name (e.g. as FUNCTION_NAME environment variable in AWS). The functions are extracted from the Spring Cloud FunctionCatalog (searching first for Function then Consumer and finally Supplier). +
    +
    +Notes on JAR Layout +You don’t need the Spring Cloud Function Web or Stream adapter at runtime in Lambda, so you might need to exclude those before you create the JAR you send to AWS. A Lambda application has to be shaded, but a Spring Boot standalone application does not, so you can run the same app using 2 separate jars (as per the sample). The sample app creates 2 jar files, one with an aws classifier for deploying in Lambda, and one executable (thin) jar that includes spring-cloud-function-web at runtime. Spring Cloud Function will try and locate a "main class" for you from the JAR file manifest, using the Start-Class attribute (which will be added for you by the Spring Boot tooling if you use the starter parent). If there is no Start-Class in your manifest you can use an environment variable MAIN_CLASS when you deploy the function to AWS. +
    +
    +Upload +Build the sample under spring-cloud-function-samples/function-sample-aws and upload the -aws jar file to Lambda. The handler can be example.Handler or org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler (FQN of the class, not a method reference, although Lambda does accept method references). +./mvnw -U clean package +Using the AWS command line tools it looks like this: +aws lambda create-function --function-name Uppercase --role arn:aws:iam::[USERID]:role/service-role/[ROLE] --zip-file fileb://function-sample-aws/target/function-sample-aws-2.0.0.BUILD-SNAPSHOT-aws.jar --handler org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler --description "Spring Cloud Function Adapter Example" --runtime java8 --region us-east-1 --timeout 30 --memory-size 1024 --publish +The input type for the function in the AWS sample is a Foo with a single property called "value". So you would need this to test it: +{ + "value": "test" +} + +The AWS sample app is written in the "functional" style (as an ApplicationContextInitializer). This is much faster on startup in Lambda than the traditional @Bean style, so if you don’t need @Beans (or @EnableAutoConfiguration) it’s a good choice. Warm starts are not affected. + +
    +
    +Platfom Specific Features +
    +HTTP and API Gateway +AWS has some platform-specific data types, including batching of messages, which is much more efficient than processing each one individually. To make use of these types you can write a function that depends on those types. Or you can rely on Spring to extract the data from the AWS types and convert it to a Spring Message. To do this you tell AWS that the function is of a specific generic handler type (depending on the AWS service) and provide a bean of type Function<Message<S>,Message<T>>, where S and T are your business data types. If there is more than one bean of type Function you may also need to configure the Spring Boot property function.name to be the name of the target bean (e.g. use FUNCTION_NAME as an environment variable). +The supported AWS services and generic handler types are listed below: + + + + + + + + +Service +AWS Types +Generic Handler + + + + + +API Gateway +APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent +org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler + + + +Kinesis +KinesisEvent +org.springframework.cloud.function.adapter.aws.SpringBootKinesisEventHandler + + + + + +For example, to deploy behind an API Gateway, use --handler org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler in your AWS command line (in via the UI) and define a @Bean of type Function<Message<Foo>,Message<Bar>> where Foo and Bar are POJO types (the data will be marshalled and unmarshalled by AWS using Jackson). +
    +
    +
    +
    +Azure Functions +The Azure adapter bootstraps a Spring Cloud Function context and channels function calls from the Azure framework into the user functions, using Spring Boot configuration where necessary. Azure Functions has quite a unique, but invasive programming model, involving annotations in user code that are specific to the platform. The easiest way to use it with Spring Cloud is to extend a base class and write a method in it with the @FunctionName annotation which delegates to a base class method. +This project provides an adapter layer for a Spring Cloud Function application onto Azure. +You can write an app with a single @Bean of type Function and it will be deployable in Azure if you get the JAR file laid out right. +There is an AzureSpringBootRequestHandler which you must extend, and provide the input and output types as annotated method parameters (enabling Azure to inspect the class and create JSON bindings). The base class has two useful methods (handleRequest and handleOutput) to which you can delegate the actual function call, so mostly the function will only ever have one line. +Example: +public class FooHandler extends AzureSpringBootRequestHandler<Foo, Bar> { + @FunctionName("uppercase") + public Bar execute( + @HttpTrigger(name = "req", methods = { HttpMethod.GET, + HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) + Foo foo, + ExecutionContext context) { + return handleRequest(foo, context); + } +} +This Azure handler will delegate to a Function<Foo,Bar> bean (or a Function<Publisher<Foo>,Publisher<Bar>>). Some Azure triggers (e.g. @CosmosDBTrigger) result in a input type of List and in that case you can bind to List in the Azure handler, or String (the raw JSON). The List input delegates to a Function with input type Map<String,Object>, or Publisher or List of the same type. The output of the Function can be a List (one-for-one) or a single value (aggregation), and the output binding in the Azure declaration should match. +If your app has more than one @Bean of type Function etc. then you can choose the one to use by configuring function.name. Or if you make the @FunctionName in the Azure handler method match the function name it should work that way (also for function apps with multiple functions). The functions are extracted from the Spring Cloud FunctionCatalog so the default function names are the same as the bean names. +
    +Notes on JAR Layout +You don’t need the Spring Cloud Function Web at runtime in Azure, so you can exclude this before you create the JAR you deploy to Azure, but it won’t be used if you include it so it doesn’t hurt to leave it in. A function application on Azure is an archive generated by the Maven plugin. The function lives in the JAR file generated by this project. The sample creates it as an executable jar, using the thin layout, so that Azure can find the handler classes. If you prefer you can just use a regular flat JAR file. The dependencies should not be included. +
    +
    +Build +./mvnw -U clean package +
    +
    +Running the sample +You can run the sample locally, just like the other Spring Cloud Function samples: + + +and curl -H "Content-Type: text/plain" localhost:8080/function -d '{"value": "hello foobar"}'. +You will need the az CLI app (see https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-java-maven for more detail). To deploy the function on Azure runtime: +$ az login +$ mvn azure-functions:deploy +On another terminal try this: curl https://<azure-function-url-from-the-log>/api/uppercase -d '{"value": "hello foobar!"}'. Please ensure that you use the right URL for the function above. Alternatively you can test the function in the Azure Dashboard UI (click on the function name, go to the right hand side and click "Test" and to the bottom right, "Run"). +The input type for the function in the Azure sample is a Foo with a single property called "value". So you need this to test it with something like below: +{ + "value": "foobar" +} + +The Azure sample app is written in the "non-functional" style (using @Bean). The functional style (with just Function or ApplicationContextInitializer) is much faster on startup in Azure than the traditional @Bean style, so if you don’t need @Beans (or @EnableAutoConfiguration) it’s a good choice. Warm starts are not affected. + +
    +
    +
    +Apache Openwhisk +The OpenWhisk adapter is in the form of an executable jar that can be used in a a docker image to be deployed to Openwhisk. The platform works in request-response mode, listening on port 8080 on a specific endpoint, so the adapter is a simple Spring MVC application. +
    +Quick Start +Implement a POF (be sure to use the functions package): +package functions; + +import java.util.function.Function; + +public class Uppercase implements Function<String, String> { + + public String apply(String input) { + return input.toUpperCase(); + } +} +Install it into your local Maven repository: +./mvnw clean install +Create a function.properties file that provides its Maven coordinates. For example: +dependencies.function: com.example:pof:0.0.1-SNAPSHOT +Copy the openwhisk runner JAR to the working directory (same directory as the properties file): +cp spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk/target/spring-cloud-function-adapter-openwhisk-2.0.0.BUILD-SNAPSHOT.jar runner.jar +Generate a m2 repo from the --thin.dryrun of the runner JAR with the above properties file: +java -jar -Dthin.root=m2 runner.jar --thin.name=function --thin.dryrun +Use the following Dockerfile: +FROM openjdk:8-jdk-alpine +VOLUME /tmp +COPY m2 /m2 +ADD runner.jar . +ADD function.properties . +ENV JAVA_OPTS="" +ENTRYPOINT [ "java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "runner.jar", "--thin.root=/m2", "--thin.name=function", "--function.name=uppercase"] +EXPOSE 8080 +
    + +you could use a Spring Cloud Function app, instead of just a jar with a POF in it, in which case you would have to change the way the app runs in the container so that it picks up the main class as a source file. For example, you could change the ENTRYPOINT above and add --spring.main.sources=com.example.SampleApplication. + +
    +Build the Docker image: +docker build -t [username/appname] . +Push the Docker image: +docker push [username/appname] +Use the OpenWhisk CLI (e.g. after vagrant ssh) to create the action: +wsk action create example --docker [username/appname] +Invoke the action: +wsk action invoke example --result --param payload foo +{ + "result": "FOO" +} +
    +
    +
    +
    + +Spring Cloud Kubernetes + +This reference guide covers how to use Spring Cloud Kubernetes. + + +Why do you need Spring Cloud Kubernetes? +Spring Cloud Kubernetes provide Spring Cloud common interface implementations that consume Kubernetes native services. +The main objective of the projects provided in this repository is to facilitate the integration of Spring Cloud and Spring Boot applications running inside Kubernetes. + + +Starters +Starters are convenient dependency descriptors you can include in your +application. Include a starter to get the dependencies and Spring Boot +auto-configuration for a feature set. + + + + + + +Starter +Features + + + + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-kubernetes</artifactId> +</dependency> +Discovery Client implementation that +resolves service names to Kubernetes Services. + + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-kubernetes-config</artifactId> +</dependency> +Load application properties from Kubernetes +ConfigMaps and Secrets. +Reload application properties when a ConfigMap or +Secret changes. + + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId> +</dependency> +Ribbon client-side load balancer with +server list obtained from Kubernetes Endpoints. + + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-kubernetes-all</artifactId> +</dependency> +All Spring Cloud Kubernetes features. + + + + + + +DiscoveryClient for Kubernetes +This project provides an implementation of Discovery Client +for Kubernetes. +This client lets you query Kubernetes endpoints (see services) by name. +A service is typically exposed by the Kubernetes API server as a collection of endpoints that represent http and https addresses and that a client can +access from a Spring Boot application running as a pod. This discovery feature is also used by the Spring Cloud Kubernetes Ribbon project +to fetch the list of the endpoints defined for an application to be load balanced. +This is something that you get for free by adding the following dependency inside your project: + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-kubernetes</artifactId> +</dependency> + +To enable loading of the DiscoveryClient, add @EnableDiscoveryClient to the according configuration or application class, as the following example shows: + +@SpringBootApplication +@EnableDiscoveryClient +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + +Then you can inject the client in your code simply by autowiring it, as the following example shows: + +@Autowired +private DiscoveryClient discoveryClient; + +You can choose to enable DiscoveryClient from all namespaces by setting the following property in application.properties: + +spring.cloud.kubernetes.discovery.all-namespaces=true + +If, for any reason, you need to disable the DiscoveryClient, you can set the following property in application.properties: + +spring.cloud.kubernetes.discovery.enabled=false + +Some Spring Cloud components use the DiscoveryClient in order to obtain information about the local service instance. For +this to work, you need to align the Kubernetes service name with the spring.application.name property. + +spring.application.name has no effect as far as the name registered for the application within Kubernetes + +Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the +DiscoveryClient implementation accordingly. In order to enable this functionality you need to add +@EnableScheduling on a configuration class in your application. + + +Kubernetes native service discovery +Kubernetes itself is capable of (server side) service discovery (see: https://kubernetes.io/docs/concepts/services-networking/service/#discovering-services). +Using native kubernetes service discovery ensures compatibility with additional tooling, such as Istio (https://istio.io), a service mesh that is capable of load balancing, ribbon, circuit breaker, failover, and much more. +The caller service then need only refer to names resolvable in a particular Kubernetes cluster. A simple implementation might use a spring RestTemplate that refers to a fully qualified domain name (FQDN), such as https://{service-name}.{namespace}.svc.{cluster}.local:{service-port}. +Additionally, you can use Hystrix for: + + +Circuit breaker implementation on the caller side, by annotating the spring boot application class with @EnableCircuitBreaker + + +Fallback functionality, by annotating the respective method with @HystrixCommand(fallbackMethod= + + + + +Kubernetes PropertySource implementations +The most common approach to configuring your Spring Boot application is to create an application.properties or applicaiton.yaml or +an application-profile.properties or application-profile.yaml file that contains key-value pairs that provide customization values to your +application or Spring Boot starters. You can override these properties by specifying system properties or environment +variables. +
    +Using a <literal>ConfigMap</literal> <literal>PropertySource</literal> +Kubernetes provides a resource named ConfigMap to externalize the +parameters to pass to your application in the form of key-value pairs or embedded application.properties or application.yaml files. +The Spring Cloud Kubernetes Config project makes Kubernetes ConfigMap instances available +during application bootstrapping and triggers hot reloading of beans or Spring context when changes are detected on +observed ConfigMap instances. +The default behavior is to create a ConfigMapPropertySource based on a Kubernetes ConfigMap that has a metadata.name value of either the name of +your Spring application (as defined by its spring.application.name property) or a custom name defined within the +bootstrap.properties file under the following key: spring.cloud.kubernetes.config.name. +However, more advanced configuration is possible where you can use multiple ConfigMap instances. +The spring.cloud.kubernetes.config.sources list makes this possible. +For example, you could define the following ConfigMap instances: + +spring: + application: + name: cloud-k8s-app + cloud: + kubernetes: + config: + name: default-name + namespace: default-namespace + sources: + # Spring Cloud Kubernetes looks up a ConfigMap named c1 in namespace default-namespace + - name: c1 + # Spring Cloud Kubernetes looks up a ConfigMap named default-name in whatever namespace n2 + - namespace: n2 + # Spring Cloud Kubernetes looks up a ConfigMap named c3 in namespace n3 + - namespace: n3 + name: c3 + +In the preceding example, if spring.cloud.kubernetes.config.namespace had not been set, +the ConfigMap named c1 would be looked up in the namespace that the application runs. +Any matching ConfigMap that is found is processed as follows: + + +Apply individual configuration properties. + + +Apply as yaml the content of any property named application.yaml. + + +Apply as a properties file the content of any property named application.properties. + + +The single exception to the aforementioned flow is when the ConfigMap contains a single key that indicates +the file is a YAML or properties file. In that case, the name of the key does NOT have to be application.yaml or +application.properties (it can be anything) and the value of the property is treated correctly. +This features facilitates the use case where the ConfigMap was created by using something like the following: + +kubectl create configmap game-config --from-file=/path/to/app-config.yaml + +Assume that we have a Spring Boot application named demo that uses the following properties to read its thread pool +configuration. + + +pool.size.core + + +pool.size.maximum + + +This can be externalized to config map in yaml format as follows: + +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + pool.size.core: 1 + pool.size.max: 16 + +Individual properties work fine for most cases. However, sometimes, embedded yaml is more convenient. In this case, we +use a single property named application.yaml to embed our yaml, as follows: + +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yaml: |- + pool: + size: + core: 1 + max:16 + +The following example also works: + +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + custom-name.yaml: |- + pool: + size: + core: 1 + max:16 + +You can also configure Spring Boot applications differently depending on active profiles that are merged together +when the ConfigMap is read. You can provide different property values for different profiles by using an +application.properties or application.yaml property, specifying profile-specific values, each in their own document +(indicated by the --- sequence), as follows: + +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yml: |- + greeting: + message: Say Hello to the World + farewell: + message: Say Goodbye + --- + spring: + profiles: development + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers + --- + spring: + profiles: production + greeting: + message: Say Hello to the Ops + +In the preceding case, the configuration loaded into your Spring Application with the development profile is as follows: + + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers + +However, if the production profile is active, the configuration becomes: + + greeting: + message: Say Hello to the Ops + farewell: + message: Say Goodbye + +If both profiles are active, the property that appears last within the ConfigMap overwrites any preceding values. +Another option is to create a different config map per profile and spring boot will automatically fetch it based +on active profiles + +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yml: |- + greeting: + message: Say Hello to the World + farewell: + message: Say Goodbye + + +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo-development +data: + application.yml: |- + spring: + profiles: development + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers + + +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo-production +data: + application.yml: |- + spring: + profiles: production + greeting: + message: Say Hello to the Ops + farewell: + message: Say Goodbye + +To tell Spring Boot which profile should be enabled at bootstrap, you can pass a system property to the Java +command. To do so, you can launch your Spring Boot application with an environment variable that you can define with the OpenShift +DeploymentConfig or Kubernetes ReplicationConfig resource file, as follows: + +apiVersion: v1 +kind: DeploymentConfig +spec: + replicas: 1 + ... + spec: + containers: + - env: + - name: JAVA_APP_DIR + value: /deployments + - name: JAVA_OPTIONS + value: -Dspring.profiles.active=developer + + +You should check the security configuration section. To access config maps from inside a pod you need to have the correct +Kubernetes service accounts, roles and role bindings. + +Another option for using ConfigMap instances is to mount them into the Pod by running the Spring Cloud Kubernetes application +and having Spring Cloud Kubernetes read them from the file system. +This behavior is controlled by the spring.cloud.kubernetes.config.paths property. You can use it in +addition to or instead of the mechanism described earlier. +You can specify multiple (exact) file paths in spring.cloud.kubernetes.config.paths by using the , delimiter. + +You have to provide the full exact path to each property file, because directories are not being recursively parsed. + + +Properties: + + + + + + + +Name +Type +Default +Description + + + + +spring.cloud.kubernetes.config.enabled +Boolean +true +Enable ConfigMaps PropertySource + + +spring.cloud.kubernetes.config.name +String +${spring.application.name} +Sets the name of ConfigMap to look up + + +spring.cloud.kubernetes.config.namespace +String +Client namespace +Sets the Kubernetes namespace where to lookup + + +spring.cloud.kubernetes.config.paths +List +null +Sets the paths where ConfigMap instances are mounted + + + + +spring.cloud.kubernetes.config.enableApi +Boolean +true +Enable or disable consuming ConfigMap instances through APIs + + + +
    +
    +
    +Secrets PropertySource +Kubernetes has the notion of Secrets for storing +sensitive data such as passwords, OAuth tokens, and so on. This project provides integration with Secrets to make secrets +accessible by Spring Boot applications. You can explicitly enable or disable This feature by setting the spring.cloud.kubernetes.secrets.enabled property. +When enabled, the SecretsPropertySource looks up Kubernetes for Secrets from the following sources: + + +Reading recursively from secrets mounts + + +Named after the application (as defined by spring.application.name) + + +Matching some labels + + +Note: +By default, consuming Secrets through the API (points 2 and 3 above) is not enabled for security reasons. The permission 'list' on secrets allows clients to inspect secrets values in the specified namespace. +Further, we recommend that containers share secrets through mounted volumes. +If you enable consuming Secrets through the API, we recommend that you limit access to Secrets by using an authorization policy, such as RBAC. +For more information about risks and best practices when consuming Secrets through the API refer to this doc. +If the secrets are found, their data is made available to the application. +Assume that we have a spring boot application named demo that uses properties to read its database +configuration. We can create a Kubernetes secret by using the following command: + +oc create secret generic db-secret --from-literal=username=user --from-literal=password=p455w0rd + +The preceding command would create the following secret (which you can see by using oc get secrets db-secret -o yaml): + +apiVersion: v1 +data: + password: cDQ1NXcwcmQ= + username: dXNlcg== +kind: Secret +metadata: + creationTimestamp: 2017-07-04T09:15:57Z + name: db-secret + namespace: default + resourceVersion: "357496" + selfLink: /api/v1/namespaces/default/secrets/db-secret + uid: 63c89263-6099-11e7-b3da-76d6186905a8 +type: Opaque + +Note that the data contains Base64-encoded versions of the literal provided by the create command. +Your application can then use this secret — for example, by exporting the secret’s value as environment variables: + +apiVersion: v1 +kind: Deployment +metadata: + name: ${project.artifactId} +spec: + template: + spec: + containers: + - env: + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: db-secret + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: db-secret + key: password + +You can select the Secrets to consume in a number of ways: + + +By listing the directories where secrets are mapped: + +-Dspring.cloud.kubernetes.secrets.paths=/etc/secrets/db-secret,etc/secrets/postgresql + +If you have all the secrets mapped to a common root, you can set them like: + +-Dspring.cloud.kubernetes.secrets.paths=/etc/secrets + + + +By setting a named secret: + +-Dspring.cloud.kubernetes.secrets.name=db-secret + + + +By defining a list of labels: + +-Dspring.cloud.kubernetes.secrets.labels.broker=activemq +-Dspring.cloud.kubernetes.secrets.labels.db=postgresql + + + + +Properties: + + + + + + + +Name +Type +Default +Description + + + + +spring.cloud.kubernetes.secrets.enabled +Boolean +true +Enable Secrets PropertySource + + +spring.cloud.kubernetes.secrets.name +String +${spring.application.name} +Sets the name of the secret to look up + + +spring.cloud.kubernetes.secrets.namespace +String +Client namespace +Sets the Kubernetes namespace where to look up + + +spring.cloud.kubernetes.secrets.labels +Map +null +Sets the labels used to lookup secrets + + +spring.cloud.kubernetes.secrets.paths +List +null +Sets the paths where secrets are mounted (example 1) + + + + +spring.cloud.kubernetes.secrets.enableApi +Boolean +false +Enables or disables consuming secrets through APIs (examples 2 and 3) + + + +
    +Notes: + + +The spring.cloud.kubernetes.secrets.labels property behaves as defined by +Map-based binding. + + +The spring.cloud.kubernetes.secrets.paths property behaves as defined by +Collection-based binding. + + +Access to secrets through the API may be restricted for security reasons. The preferred way is to mount secrets to the Pod. + + +You can find an example of an application that uses secrets (though it has not been updated to use the new spring-cloud-kubernetes project) at +spring-boot-camel-config +
    +
    +<literal>PropertySource</literal> Reload +Some applications may need to detect changes on external property sources and update their internal status to reflect the new configuration. +The reload feature of Spring Cloud Kubernetes is able to trigger an application reload when a related ConfigMap or +Secret changes. +By default, this feature is disabled. You can enable it by using the spring.cloud.kubernetes.reload.enabled=true configuration property (for example, in the application.properties file). +The following levels of reload are supported (by setting the spring.cloud.kubernetes.reload.strategy property): +* refresh (default): Only configuration beans annotated with @ConfigurationProperties or @RefreshScope are reloaded. +This reload level leverages the refresh feature of Spring Cloud Context. +* restart_context: the whole Spring ApplicationContext is gracefully restarted. Beans are recreated with the new configuration. +* shutdown: the Spring ApplicationContext is shut down to activate a restart of the container. + When you use this level, make sure that the lifecycle of all non-daemon threads is bound to the ApplicationContext +and that a replication controller or replica set is configured to restart the pod. +Assuming that the reload feature is enabled with default settings (refresh mode), the following bean is refreshed when the config map changes: + +@Configuration +@ConfigurationProperties(prefix = "bean") +public class MyConfig { + + private String message = "a message that can be changed live"; + + // getter and setters + +} + +To see that changes effectively happen, you can create another bean that prints the message periodically, as follows + +@Component +public class MyBean { + + @Autowired + private MyConfig config; + + @Scheduled(fixedDelay = 5000) + public void hello() { + System.out.println("The message is: " + config.getMessage()); + } +} + +You can change the message printed by the application by using a ConfigMap, as follows: + +apiVersion: v1 +kind: ConfigMap +metadata: + name: reload-example +data: + application.properties: |- + bean.message=Hello World! + +Any change to the property named bean.message in the ConfigMap associated with the pod is reflected in the +output. More generally speaking, changes associated to properties prefixed with the value defined by the prefix +field of the @ConfigurationProperties annotation are detected and reflected in the application. +Associating a ConfigMap with a pod is explained earlier in this chapter. +The full example is available in spring-cloud-kubernetes-reload-example. +The reload feature supports two operating modes: +* Event (default): Watches for changes in config maps or secrets by using the Kubernetes API (web socket). +Any event produces a re-check on the configuration and, in case of changes, a reload. +The view role on the service account is required in order to listen for config map changes. A higher level role (such as edit) is required for secrets +(by default, secrets are not monitored). +* Polling: Oeriodically re-creates the configuration from config maps and secrets to see if it has changed. +You can configure the polling period by using the spring.cloud.kubernetes.reload.period property and defaults to 15 seconds. +It requires the same role as the monitored property source. +This means, for example, that using polling on file-mounted secret sources does not require particular privileges. + +Properties: + + + + + + + +Name +Type +Default +Description + + + + +spring.cloud.kubernetes.reload.enabled +Boolean +false +Enables monitoring of property sources and configuration reload + + +spring.cloud.kubernetes.reload.monitoring-config-maps +Boolean +true +Allow monitoring changes in config maps + + +spring.cloud.kubernetes.reload.monitoring-secrets +Boolean +false +Allow monitoring changes in secrets + + +`spring.cloud.kubernetes.reload.strategy ` +Enum +refresh +The strategy to use when firing a reload (refresh, restart_context, or shutdown) + + +spring.cloud.kubernetes.reload.mode +Enum +event +Specifies how to listen for changes in property sources (event or polling) + + + + +spring.cloud.kubernetes.reload.period +Duration +15s +The period for verifying changes when using the polling strategy + + + +
    +Notes: +* You should not use properties under spring.cloud.kubernetes.reload in config maps or secrets. Changing such properties at runtime may lead to unexpected results. +* Deleting a property or the whole config map does not restore the original state of the beans when you use the refresh level. +
    +
    + +Ribbon Discovery in Kubernetes +Spring Cloud client applications that call a microservice should be interested on relying on a client load-balancing +feature in order to automatically discover at which endpoint(s) it can reach a given service. This mechanism has been +implemented within the spring-cloud-kubernetes-ribbon project, where a +Kubernetes client populates a Ribbon ServerList that contains information +about such endpoints. +The implementation is part of the following starter that you can use by adding its dependency to your pom file: + +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId> + <version>${latest.version}</version> +</dependency> + +When the list of the endpoints is populated, the Kubernetes client searches the registered endpoints that live in +the current namespace or project by matching the service name defined in the Ribbon Client annotation, as follows: + +@RibbonClient(name = "name-service") + +You can configure Ribbon’s behavior by providing properties in your application.properties (through your application’s +dedicated ConfigMap) by using the following format: <name of your service>.ribbon.<Ribbon configuration key>, where: + + +<name of your service> corresponds to the service name you access over Ribbon, as configured by using the +@RibbonClient annotation (such as name-service in the preceding example). + + +<Ribbon configuration key> is one of the Ribbon configuration keys defined by +Ribbon’s CommonClientConfigKey class. + + +Additionally, the spring-cloud-kubernetes-ribbon project defines two additional configuration keys to further +control how Ribbon interacts with Kubernetes. In particular, if an endpoint defines multiple ports, the default +behavior is to use the first one found. To select more specifically which port to use in a multi-port service, you can use +the PortName key. If you want to specify in which Kubernetes namespace the target service should be looked up, you can use +the KubernetesNamespace key, remembering in both instances to prefix these keys with your service name and +ribbon prefix, as specified earlier. +The following examples use this module for ribbon discovery: + + +Spring Cloud Circuitbreaker and Ribbon + + +fabric8-quickstarts - Spring Boot - Ribbon + + +Kubeflix - LoanBroker - Bank + + + +You can disable the Ribbon discovery client by setting the spring.cloud.kubernetes.ribbon.enabled=false key within the application properties file. + + + +Kubernetes Ecosystem Awareness +All of the features described earlier in this guide work equally well, regardless of whether your application is running inside +Kubernetes. This is really helpful for development and troubleshooting. +From a development point of view, this lets you start your Spring Boot application and debug one +of the modules that is part of this project. You need not deploy it in Kubernetes, +as the code of the project relies on the +Fabric8 Kubernetes Java client, which is a fluent DSL that can +communicate by using http protocol to the REST API of the Kubernetes Server. +To disable the integration with Kubernetes you can set spring.cloud.kubernetes.enabled to false. Please be aware that when spring-cloud-kubernetes-config is on the classpath, +spring.cloud.kubernetes.enabled should be set in bootstrap.{properties|yml} (or the profile specific one) otherwise it should be in application.{properties|yml} (or the profile specific one). +Also note that these properties: spring.cloud.kubernetes.config.enabled and spring.cloud.kubernetes.secrets.enabled only take effect when set in bootstrap.{properties|yml} +
    +Kubernetes Profile Autoconfiguration +When the application runs as a pod inside Kubernetes, a Spring profile named kubernetes automatically gets activated. +This lets you customize the configuration, to define beans that are applied when the Spring Boot application is deployed +within the Kubernetes platform (for example, different development and production configuration). +
    +
    +Istio Awareness +When you include the spring-cloud-kubernetes-istio module in the application classpath, a new profile is added to the application, +provided the application is running inside a Kubernetes Cluster with Istio installed. You can then use +spring @Profile("istio") annotations in your Beans and @Configuration classes. +The Istio awareness module uses me.snowdrop:istio-client to interact with Istio APIs, letting us discover traffic rules, circuit breakers, and so on, +making it easy for our Spring Boot applications to consume this data to dynamically configure themselves according to the environment. +
    +
    + +Pod Health Indicator +Spring Boot uses HealthIndicator to expose info about the health of an application. +That makes it really useful for exposing health-related information to the user and makes it a good fit for use as readiness probes. +The Kubernetes health indicator (which is part of the core module) exposes the following info: + + +Pod name, IP address, namespace, service account, node name, and its IP address + + +A flag that indicates whether the Spring Boot application is internal or external to Kubernetes + + + + +Leader Election +<TBD> + + +Security Configurations Inside Kubernetes +
    +Namespace +Most of the components provided in this project need to know the namespace. For Kubernetes (1.3+), the namespace is made available to the pod as part of the service account secret and is automatically detected by the client. +For earlier versions, it needs to be specified as an environment variable to the pod. A quick way to do this is as follows: + + env: + - name: "KUBERNETES_NAMESPACE" + valueFrom: + fieldRef: + fieldPath: "metadata.namespace" + +
    +
    +Service Account +For distributions of Kubernetes that support more fine-grained role-based access within the cluster, you need to make sure a pod that runs with spring-cloud-kubernetes has access to the Kubernetes API. +For any service accounts you assign to a deployment or pod, you need to make sure they have the correct roles. For example, you can add cluster-reader permissions to your default service account, depending on the project you’re in. +
    +
    + +Service Registry Implementation +In Kubernetes service registration is controlled by the platform, the application itself does not control +registration as it may do in other platforms. For this reason using spring.cloud.service-registry.auto-registration.enabled +or setting @EnableDiscoveryClient(autoRegister=false) will have no effect in Spring Cloud Kubernetes. + + +Examples +Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by +following the Spring Cloud interfaces. +In your applications, you need to add the spring-cloud-kubernetes-discovery dependency to your classpath and remove any other dependency that contains a DiscoveryClient implementation (that is, a Eureka discovery client). +The same applies for PropertySourceLocator, where you need to add to the classpath the spring-cloud-kubernetes-config and remove any other dependency that contains a PropertySourceLocator implementation (that is, a configuration server client). +The following projects highlight the usage of these dependencies and demonstrate how you can use these libraries from any Spring Boot application: + + +Spring Cloud Kubernetes Examples: the ones located inside this repository. + + +Spring Cloud Kubernetes Full Example: Minions and Boss + + +Minion + + +Boss + + + + +Spring Cloud Kubernetes Full Example: SpringOne Platform Tickets Service + + +Spring Cloud Gateway with Spring Cloud Kubernetes Discovery and Config + + +Spring Boot Admin with Spring Cloud Kubernetes Discovery and Config + + + + +Other Resources +This section lists other resources, such as presentations (slides) and videos about Spring Cloud Kubernetes. + + +S1P Spring Cloud on PKS + + +Spring Cloud, Docker, Kubernetes → London Java Community July 2018 + + +Please feel free to submit other resources through pull requests to this repository. + + +Building +
    +Basic Compile and Test +To build the source you will need to install JDK 1.7. +Spring Cloud uses Maven for most build-related activities, and you +should be able to get off the ground quite quickly by cloning the +project you are interested in and typing +$ ./mvnw install + +You can also install Maven (>=3.3.3) yourself and run the mvn command +in place of ./mvnw in the examples below. If you do that you also +might need to add -P spring if your local Maven settings do not +contain repository declarations for spring pre-release artifacts. + + +Be aware that you might need to increase the amount of memory +available to Maven by setting a MAVEN_OPTS environment variable with +a value like -Xmx512m -XX:MaxPermSize=128m. We try to cover this in +the .mvn configuration, so if you find you have to do it to make a +build succeed, please raise a ticket to get the settings added to +source control. + +For hints on how to build the project look in .travis.yml if there +is one. There should be a "script" and maybe "install" command. Also +look at the "services" section to see if any services need to be +running locally (e.g. mongo or rabbit). Ignore the git-related bits +that you might find in "before_install" since they’re related to setting git +credentials and you already have those. +The projects that require middleware generally include a +docker-compose.yml, so consider using +Docker Compose to run the middeware servers +in Docker containers. See the README in the +scripts demo +repository for specific instructions about the common cases of mongo, +rabbit and redis. + +If all else fails, build with the command from .travis.yml (usually +./mvnw install). + +
    +
    +Documentation +The spring-cloud-build module has a "docs" profile, and if you switch +that on it will try to build asciidoc sources from +src/main/asciidoc. As part of that process it will look for a +README.adoc and process it by loading all the includes, but not +parsing or rendering it, just copying it to ${main.basedir} +(defaults to $../../../.., i.e. the root of the project). If there are +any changes in the README it will then show up after a Maven build as +a modified file in the correct place. Just commit it and push the change. +
    +
    +Working with the code +If you don’t have an IDE preference we would recommend that you use +Spring Tools Suite or +Eclipse when working with the code. We use the +m2eclipse eclipse plugin for maven support. Other IDEs and tools +should also work without issue as long as they use Maven 3.3.3 or better. +
    +Importing into eclipse with m2eclipse +We recommend the m2eclipse eclipse plugin when working with +eclipse. If you don’t already have m2eclipse installed it is available from the "eclipse +marketplace". + +Older versions of m2e do not support Maven 3.3, so once the +projects are imported into Eclipse you will also need to tell +m2eclipse to use the right profile for the projects. If you +see many different errors related to the POMs in the projects, check +that you have an up to date installation. If you can’t upgrade m2e, +add the "spring" profile to your settings.xml. Alternatively you can +copy the repository settings from the "spring" profile of the parent +pom into your settings.xml. + +
    +
    +Importing into eclipse without m2eclipse +If you prefer not to use m2eclipse you can generate eclipse project metadata using the +following command: +$ ./mvnw eclipse:eclipse +The generated eclipse projects can be imported by selecting import existing projects +from the file menu. +
    +
    +
    + +Contributing +Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below. +
    +Sign the Contributor License Agreement +Before we accept a non-trivial patch or pull request we will need you to sign the +Contributor License Agreement. +Signing the contributor’s agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests. +
    +
    +Code of Conduct +This project adheres to the Contributor Covenant code of +conduct. By participating, you are expected to uphold this code. Please report +unacceptable behavior to spring-code-of-conduct@pivotal.io. +
    +
    +Code Conventions and Housekeeping +None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge. + + +Use the Spring Framework code format conventions. If you use Eclipse +you can import formatter settings using the +eclipse-code-formatter.xml file from the +Spring +Cloud Build project. If using IntelliJ, you can use the +Eclipse Code Formatter +Plugin to import the same file. + + +Make sure all new .java files to have a simple Javadoc class comment with at least an +@author tag identifying you, and preferably at least a paragraph on what the class is +for. + + +Add the ASF license header comment to all new .java files (copy from existing files +in the project) + + +Add yourself as an @author to the .java files that you modify substantially (more +than cosmetic changes). + + +Add some Javadocs and, if you change the namespace, some XSD doc elements. + + +A few unit tests would help a lot as well — someone has to do it. + + +If no-one else is using your branch, please rebase it against the current master (or +other target branch in the main project). + + +When writing a commit message please follow these conventions, +if you are fixing an existing issue please add Fixes gh-XXXX at the end of the commit +message (where XXXX is the issue number). + + +
    +
    +Checkstyle +Spring Cloud Build comes with a set of checkstyle rules. You can find them in the spring-cloud-build-tools module. The most notable files under the module are: + +spring-cloud-build-tools/ + +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml +    └── main +    └── resources +    ├── checkstyle-header.txt +    └── checkstyle.xml + + + + +Default Checkstyle rules + + +File header setup + + +Default suppression rules + + +
    +Checkstyle configuration +Checkstyle rules are disabled by default. To add checkstyle to your project just define the following properties and plugins. + +pom.xml + +<properties> +<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError> + <maven-checkstyle-plugin.failsOnViolation>true + </maven-checkstyle-plugin.failsOnViolation> + <maven-checkstyle-plugin.includeTestSourceDirectory>true + </maven-checkstyle-plugin.includeTestSourceDirectory> +</properties> + +<build> + <plugins> + <plugin> + <groupId>io.spring.javaformat</groupId> + <artifactId>spring-javaformat-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + </plugin> + </plugins> + + <reporting> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + </plugin> + </plugins> + </reporting> +</build> + + + + +Fails the build upon Checkstyle errors + + +Fails the build upon Checkstyle violations + + +Checkstyle analyzes also the test sources + + +Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules + + +Add checkstyle plugin to your build and reporting phases + + +If you need to suppress some rules (e.g. line length needs to be longer), then it’s enough for you to define a file under ${project.root}/src/checkstyle/checkstyle-suppressions.xml with your suppressions. Example: + +projectRoot/src/checkstyle/checkstyle-suppresions.xml + +<?xml version="1.0"?> +<!DOCTYPE suppressions PUBLIC + "-//Puppy Crawl//DTD Suppressions 1.1//EN" + "https://www.puppycrawl.com/dtds/suppressions_1_1.dtd"> +<suppressions> + <suppress files=".*ConfigServerApplication\.java" checks="HideUtilityClassConstructor"/> + <suppress files=".*ConfigClientWatch\.java" checks="LineLengthCheck"/> +</suppressions> + + +It’s advisable to copy the ${spring-cloud-build.rootFolder}/.editorconfig and ${spring-cloud-build.rootFolder}/.springformat to your project. That way, some default formatting rules will be applied. You can do so by running this script: +$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig +$ touch .springformat +
    +
    +
    +IDE setup +
    +Intellij IDEA +In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. +The following files can be found in the Spring Cloud Build project. + +spring-cloud-build-tools/ + +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml +    └── main +    └── resources +    ├── checkstyle-header.txt +    ├── checkstyle.xml +    └── intellij +       ├── Intellij_Project_Defaults.xml +       └── Intellij_Spring_Boot_Java_Conventions.xml + + + + +Default Checkstyle rules + + +File header setup + + +Default suppression rules + + +Project defaults for Intellij that apply most of Checkstyle rules + + +Project style conventions for Intellij that apply most of Checkstyle rules + + +
    +Code style + + + + +Code style + +
    +Go to FileSettingsEditorCode style. There click on the icon next to the Scheme section. There, click on the Import Scheme value and pick the Intellij IDEA code style XML option. Import the spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml file. +
    +Inspection profiles + + + + +Code style + +
    +Go to FileSettingsEditorInspections. There click on the icon next to the Profile section. There, click on the Import Profile and import the spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml file. + +Checkstyle +To have Intellij work with Checkstyle, you have to install the Checkstyle plugin. It’s advisable to also install the Assertions2Assertj to automatically convert the JUnit assertions + + + + + + +Checkstyle + + +Go to FileSettingsOther settingsCheckstyle. There click on the + icon in the Configuration file section. There, you’ll have to define where the checkstyle rules should be picked from. In the image above, we’ve picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build’s GitHub repository (e.g. for the checkstyle.xml : https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml). We need to provide the following variables: + + +checkstyle.header.file - please point it to the Spring Cloud Build’s, spring-cloud-build-tools/src/main/resources/checkstyle-header.txt file either in your cloned repo or via the https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt URL. + + +checkstyle.suppressions.file - default suppressions. Please point it to the Spring Cloud Build’s, spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml file either in your cloned repo or via the https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml URL. + + +checkstyle.additional.suppressions.file - this variable corresponds to suppressions in your local project. E.g. you’re working on spring-cloud-contract. Then point to the project-root/src/checkstyle/checkstyle-suppressions.xml folder. Example for spring-cloud-contract would be: /home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml. + + + +Remember to set the Scan Scope to All sources since we apply checkstyle rules for production and test sources. + +
    +
    +
    +
    + +Spring Cloud GCP + +João André Martins; Jisha Abubaker; Ray Tsang; Mike Eltsufin; Artem Bilan; Andreas Berger; Balint Pato; Chengyuan Zhao; Dmitry Solomakha; Elena Felder; Daniel Zou + + +Introduction +The Spring Cloud GCP project makes the Spring Framework a first-class citizen of Google Cloud Platform (GCP). +Spring Cloud GCP lets you leverage the power and simplicity of the Spring Framework to: + + +Analyze your images for text, objects, and other content with Google Cloud Vision + + +Use Spring Security via Google Cloud IAP + + +Map objects, relationships, and collections with Spring Data Cloud Spanner and Spring Data Cloud Datastore + + +Publish and subscribe to Google Cloud Pub/Sub topics + + +Configure Spring JDBC with a few properties to use Google Cloud SQL + + +Write and read from Spring Resources backed up by Google Cloud Storage + + +Exchange messages with Spring Integration using Google Cloud Pub/Sub on the background + + +Trace the execution of your app with Spring Cloud Sleuth and Google Stackdriver Trace + + +Configure your app with Spring Cloud Config, backed up by the Google Runtime Configuration API + + +Consume and produce Google Cloud Storage data via Spring Integration GCS Channel Adapters + + + + +Dependency Management +The Spring Cloud GCP Bill of Materials (BOM) contains the versions of all the dependencies it uses. +If you’re a Maven user, adding the following to your pom.xml file will allow you to not specify any Spring Cloud GCP dependency versions. +Instead, the version of the BOM you’re using determines the versions of the used dependencies. +<dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-dependencies</artifactId> + <version>{project-version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> +</dependencyManagement> +In the following sections, it will be assumed you are using the Spring Cloud GCP BOM and the dependency snippets will not contain versions. +Gradle users can achieve the same kind of BOM experience using Spring’s dependency-management-plugin Gradle plugin. +For simplicity, the Gradle dependency snippets in the remainder of this document will also omit their versions. + + +Getting started +There are many available resources to get you up to speed with our libraries as quickly as possible. +
    +Spring Initializr +There are three entries in Spring Initializr for Spring Cloud GCP. +
    +GCP Support +The GCP Support entry contains auto-configuration support for every Spring Cloud GCP integration. +Most of the autoconfiguration code is only enabled if other dependencies are added to the classpath. + + + + + + +Spring Cloud GCP Starter +Required dependencies + + + + +Config +org.springframework.cloud:spring-cloud-gcp-starter-config + + +Cloud Spanner +org.springframework.cloud:spring-cloud-gcp-starter-data-spanner + + +Cloud Datastore +org.springframework.cloud:spring-cloud-gcp-starter-data-datastore + + +Logging +org.springframework.cloud:spring-cloud-gcp-starter-logging + + +SQL - MySql +org.springframework.cloud:spring-cloud-gcp-starter-sql-mysql + + +SQL - PostgreSQL +org.springframework.cloud:spring-cloud-gcp-starter-sql-postgres + + +Trace +org.springframework.cloud:spring-cloud-gcp-starter-trace + + +Vision +org.springframework.cloud:spring-cloud-gcp-starter-vision + + +Security - IAP +org.springframework.cloud:spring-cloud-gcp-starter-security-iap + + + + +
    +
    +GCP Messaging +The GCP Messaging entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Pub/Sub integrations work out of the box. +
    +
    +GCP Storage +The GCP Storage entry adds the GCP Support entry and all the required dependencies so that the Google Cloud Storage integrations work out of the box. +
    +
    +
    +Code Samples +There are code samples available that demonstrate the usage of all our integrations. +For example, the Vision API sample shows how to use spring-cloud-gcp-starter-vision to automatically configure Vision API clients. +
    +
    +Code Challenges +In a code challenge, you perform a task step by step, using one integration. +There are a number of challenges available in the Google Developers Codelabs page. +
    +
    +Getting Started Guides +A Spring Getting Started guide on messaging with Spring Integration Channel Adapters for Google Cloud Pub/Sub is available from Spring Guides. +
    +
    + +Spring Cloud GCP Core +Each Spring Cloud GCP module uses GcpProjectIdProvider and CredentialsProvider to get the GCP project ID and access credentials. +Spring Cloud GCP provides a Spring Boot starter to auto-configure the core components. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter' +} +
    +Project ID +GcpProjectIdProvider is a functional interface that returns a GCP project ID string. +public interface GcpProjectIdProvider { + String getProjectId(); +} +The Spring Cloud GCP starter auto-configures a GcpProjectIdProvider. +If a spring.cloud.gcp.project-id property is specified, the provided GcpProjectIdProvider returns that property value. +spring.cloud.gcp.project-id=my-gcp-project-id +Otherwise, the project ID is discovered based on an +ordered list of rules: + + +The project ID specified by the GOOGLE_CLOUD_PROJECT environment variable + + +The Google App Engine project ID + + +The project ID specified in the JSON credentials file pointed by the GOOGLE_APPLICATION_CREDENTIALS environment variable + + +The Google Cloud SDK project ID + + +The Google Compute Engine project ID, from the Google Compute Engine Metadata Server + + +
    +
    +Credentials +CredentialsProvider is a functional interface that returns the credentials to authenticate and authorize calls to Google Cloud Client Libraries. +public interface CredentialsProvider { + Credentials getCredentials() throws IOException; +} +The Spring Cloud GCP starter auto-configures a CredentialsProvider. +It uses the spring.cloud.gcp.credentials.location property to locate the OAuth2 private key of a Google service account. +Keep in mind this property is a Spring Resource, so the credentials file can be obtained from a number of different locations such as the file system, classpath, URL, etc. +The next example specifies the credentials location property in the file system. +spring.cloud.gcp.credentials.location=file:/usr/local/key.json +Alternatively, you can set the credentials by directly specifying the spring.cloud.gcp.credentials.encoded-key property. +The value should be the base64-encoded account private key in JSON format. +If that credentials aren’t specified through properties, the starter tries to discover credentials from a number of places: + + +Credentials file pointed to by the GOOGLE_APPLICATION_CREDENTIALS environment variable + + +Credentials provided by the Google Cloud SDK gcloud auth application-default login command + + +Google App Engine built-in credentials + + +Google Cloud Shell built-in credentials + + +Google Compute Engine built-in credentials + + +If your app is running on Google App Engine or Google Compute Engine, in most cases, you should omit the spring.cloud.gcp.credentials.location property and, instead, let the Spring Cloud GCP Starter get the correct credentials for those environments. +On App Engine Standard, the App Identity service account credentials are used, on App Engine Flexible, the Flexible service account credential are used and on Google Compute Engine, the Compute Engine Default Service Account is used. +
    +Scopes +By default, the credentials provided by the Spring Cloud GCP Starter contain scopes for every service supported by Spring Cloud GCP. + + + + + + +Service +Scope + + +Spanner +https://www.googleapis.com/auth/spanner.admin, https://www.googleapis.com/auth/spanner.data + + +Datastore +https://www.googleapis.com/auth/datastore + + +Pub/Sub +https://www.googleapis.com/auth/pubsub + + +Storage (Read Only) +https://www.googleapis.com/auth/devstorage.read_only + + +Storage (Write/Write) +https://www.googleapis.com/auth/devstorage.read_write + + +Runtime Config +https://www.googleapis.com/auth/cloudruntimeconfig + + +Trace (Append) +https://www.googleapis.com/auth/trace.append + + +Cloud Platform +https://www.googleapis.com/auth/cloud-platform + + +Vision +https://www.googleapis.com/auth/cloud-vision + + + + +The Spring Cloud GCP starter allows you to configure a custom scope list for the provided credentials. +To do that, specify a comma-delimited list of Google OAuth2 scopes in the spring.cloud.gcp.credentials.scopes property. +spring.cloud.gcp.credentials.scopes is a comma-delimited list of Google OAuth2 scopes for Google Cloud Platform services that the credentials returned by the provided CredentialsProvider support. +spring.cloud.gcp.credentials.scopes=https://www.googleapis.com/auth/pubsub,https://www.googleapis.com/auth/sqlservice.admin +You can also use DEFAULT_SCOPES placeholder as a scope to represent the starters default scopes, and append the additional scopes you need to add. +spring.cloud.gcp.credentials.scopes=DEFAULT_SCOPES,https://www.googleapis.com/auth/cloud-vision +
    +
    +
    +Environment +GcpEnvironmentProvider is a functional interface, auto-configured by the Spring Cloud GCP starter, that returns a GcpEnvironment enum. +The provider can help determine programmatically in which GCP environment (App Engine Flexible, App Engine Standard, Kubernetes Engine or Compute Engine) the application is deployed. +public interface GcpEnvironmentProvider { + GcpEnvironment getCurrentEnvironment(); +} +
    +
    +Spring Initializr +This starter is available from Spring Initializr through the GCP Support entry. +
    +
    + +Google Cloud Pub/Sub +Spring Cloud GCP provides an abstraction layer to publish to and subscribe from Google Cloud Pub/Sub topics and to create, list or delete Google Cloud Pub/Sub topics and subscriptions. +A Spring Boot starter is provided to auto-configure the various required Pub/Sub components. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-pubsub</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-pubsub' +} +This starter is also available from Spring Initializr through the GCP Messaging entry. +
    +Pub/Sub Operations & Template +PubSubOperations is an abstraction that allows Spring users to use Google Cloud Pub/Sub without depending on any Google Cloud Pub/Sub API semantics. +It provides the common set of operations needed to interact with Google Cloud Pub/Sub. +PubSubTemplate is the default implementation of PubSubOperations and it uses the Google Cloud Java Client for Pub/Sub to interact with Google Cloud Pub/Sub. +PubSubTemplate depends on a PublisherFactory and a SubscriberFactory. +The PublisherFactory provides a Google Cloud Java Client for Pub/Sub Publisher. +The SubscriberFactory provides the Subscriber for asynchronous message pulling, as well as a SubscriberStub for synchronous pulling. +The Spring Boot starter for GCP Pub/Sub auto-configures a PublisherFactory and SubscriberFactory with default settings and uses the GcpProjectIdProvider and CredentialsProvider auto-configured by the Spring Boot GCP starter. +The PublisherFactory implementation provided by Spring Cloud GCP Pub/Sub, DefaultPublisherFactory, caches Publisher instances by topic name, in order to optimize resource utilization. +The PubSubOperations interface is actually a combination of PubSubPublisherOperations and PubSubSubscriberOperations with the corresponding PubSubPublisherTemplate and PubSubSubscriberTemplate implementations, which can be used individually or via the composite PubSubTemplate. +The rest of the documentation refers to PubSubTemplate, but the same applies to PubSubPublisherTemplate and PubSubSubscriberTemplate, depending on whether we’re talking about publishing or subscribing. +
    +Publishing to a topic +PubSubTemplate provides asynchronous methods to publish messages to a Google Cloud Pub/Sub topic. +The publish() method takes in a topic name to post the message to, a payload of a generic type and, optionally, a map with the message headers. +Here is an example of how to publish a message to a Google Cloud Pub/Sub topic: +public void publishMessage() { + this.pubSubTemplate.publish("topic", "your message payload", ImmutableMap.of("key1", "val1")); +} +By default, the SimplePubSubMessageConverter is used to convert payloads of type byte[], ByteString, ByteBuffer, and String to Pub/Sub messages. +
    +JSON support +For serialization and deserialization of POJOs using Jackson JSON, configure a JacksonPubSubMessageConverter bean, and the Spring Boot starter for GCP Pub/Sub will automatically wire it into the PubSubTemplate. +// Note: The ObjectMapper is used to convert Java POJOs to and from JSON. +// You will have to configure your own instance if you are unable to depend +// on the ObjectMapper provided by Spring Boot starters. +@Bean +public JacksonPubSubMessageConverter jacksonPubSubMessageConverter(ObjectMapper objectMapper) { + return new JacksonPubSubMessageConverter(objectMapper); +} +Alternatively, you can set it directly by calling the setMessageConverter() method on the PubSubTemplate. +Other implementations of the PubSubMessageConverter can also be configured in the same manner. +Please refer to our Pub/Sub JSON Payload Sample App as a reference for using this functionality. +
    +
    +
    +Subscribing to a subscription +Google Cloud Pub/Sub allows many subscriptions to be associated to the same topic. +PubSubTemplate allows you to listen to subscriptions via the subscribe() method. +It relies on a SubscriberFactory object, whose only task is to generate Google Cloud Pub/Sub +Subscriber objects. +When listening to a subscription, messages will be pulled from Google Cloud Pub/Sub +asynchronously, at a certain interval. +The Spring Boot starter for Google Cloud Pub/Sub auto-configures a SubscriberFactory. +If Pub/Sub message payload conversion is desired, you can use the subscribeAndConvert() method, which will use the converter configured in the template. +
    +
    +Pulling messages from a subscription +Google Cloud Pub/Sub supports synchronous pulling of messages from a subscription. +This is different from subscribing to a subscription, in the sense that subscribing is an asynchronous task which polls the subscription on a set interval. +The pullNext() method allows for a single message to be pulled and automatically acknowledged from a subscription. +The pull() method pulls a number of messages from a subscription, allowing for the retry settings to be configured. +Any messages received by pull() are not automatically acknowledged. +Instead, since they are of the kind AcknowledgeablePubsubMessage, you can acknowledge them by calling the ack() method, or negatively acknowledge them by calling the nack() method. +The pullAndAck() method does the same as the pull() method and, additionally, acknowledges all received messages. +The pullAndConvert() method does the same as the pull() method and, additionally, converts the Pub/Sub binary payload to an object of the desired type, using the converter configured in the template. +To acknowledge multiple messages received from pull() or pullAndConvert() at once, you can use the PubSubTemplate.ack() method. +You can also use the PubSubTemplate.nack() for negatively acknowledging messages. +Using these methods for acknowledging messages in batches is more efficient than acknowledging messages individually, but they require the collection of messages to be from the same project. +All ack(), nack(), and modifyAckDeadline() methods on messages as well as PubSubSubscriberTemplate are implemented asynchronously, returning a ListenableFuture<Void> to be able to process the asynchronous execution. +PubSubTemplate uses a special subscriber generated by its SubscriberFactory to synchronously pull messages. +
    +
    +
    +Pub/Sub management +PubSubAdmin is the abstraction provided by Spring Cloud GCP to manage Google Cloud Pub/Sub resources. +It allows for the creation, deletion and listing of topics and subscriptions. +PubSubAdmin depends on GcpProjectIdProvider and either a CredentialsProvider or a TopicAdminClient and a SubscriptionAdminClient. +If given a CredentialsProvider, it creates a TopicAdminClient and a SubscriptionAdminClient with the Google Cloud Java Library for Pub/Sub default settings. +The Spring Boot starter for GCP Pub/Sub auto-configures a PubSubAdmin object using the GcpProjectIdProvider and the CredentialsProvider auto-configured by the Spring Boot GCP Core starter. +
    +Creating a topic +PubSubAdmin implements a method to create topics: +public Topic createTopic(String topicName) +Here is an example of how to create a Google Cloud Pub/Sub topic: +public void newTopic() { + pubSubAdmin.createTopic("topicName"); +} +
    +
    +Deleting a topic +PubSubAdmin implements a method to delete topics: +public void deleteTopic(String topicName) +Here is an example of how to delete a Google Cloud Pub/Sub topic: +public void deleteTopic() { + pubSubAdmin.deleteTopic("topicName"); +} +
    +
    +Listing topics +PubSubAdmin implements a method to list topics: +public List<Topic> listTopics +Here is an example of how to list every Google Cloud Pub/Sub topic name in a project: +public List<String> listTopics() { + return pubSubAdmin + .listTopics() + .stream() + .map(Topic::getNameAsTopicName) + .map(TopicName::getTopic) + .collect(Collectors.toList()); +} +
    +
    +Creating a subscription +PubSubAdmin implements a method to create subscriptions to existing topics: +public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline, String pushEndpoint) +Here is an example of how to create a Google Cloud Pub/Sub subscription: +public void newSubscription() { + pubSubAdmin.createSubscription("subscriptionName", "topicName", 10, “https://my.endpoint/push”); +} +Alternative methods with default settings are provided for ease of use. +The default value for ackDeadline is 10 seconds. +If pushEndpoint isn’t specified, the subscription uses message pulling, instead. +public Subscription createSubscription(String subscriptionName, String topicName) +public Subscription createSubscription(String subscriptionName, String topicName, Integer ackDeadline) +public Subscription createSubscription(String subscriptionName, String topicName, String pushEndpoint) +
    +
    +Deleting a subscription +PubSubAdmin implements a method to delete subscriptions: +public void deleteSubscription(String subscriptionName) +Here is an example of how to delete a Google Cloud Pub/Sub subscription: +public void deleteSubscription() { + pubSubAdmin.deleteSubscription("subscriptionName"); +} +
    +
    +Listing subscriptions +PubSubAdmin implements a method to list subscriptions: +public List<Subscription> listSubscriptions() +Here is an example of how to list every subscription name in a project: +public List<String> listSubscriptions() { + return pubSubAdmin + .listSubscriptions() + .stream() + .map(Subscription::getNameAsSubscriptionName) + .map(SubscriptionName::getSubscription) + .collect(Collectors.toList()); +} +
    +
    +
    +Configuration +The Spring Boot starter for Google Cloud Pub/Sub provides the following configuration options: + + + + + + + + +Name +Description +Required +Default value + + +spring.cloud.gcp.pubsub.enabled +Enables or disables Pub/Sub auto-configuration +No +true + + +spring.cloud.gcp.pubsub.subscriber.executor-threads +Number of threads used by Subscriber instances created by SubscriberFactory +No +4 + + +spring.cloud.gcp.pubsub.publisher.executor-threads +Number of threads used by Publisher instances created by PublisherFactory +No +4 + + +spring.cloud.gcp.pubsub.project-id +GCP project ID where the Google Cloud Pub/Sub API is hosted, if different from the one in the Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.pubsub.credentials.location +OAuth2 credentials for authenticating with the +Google Cloud Pub/Sub API, if different from the ones in the +Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.pubsub.credentials.encoded-key +Base64-encoded contents of OAuth2 account private key for authenticating with the +Google Cloud Pub/Sub API, if different from the ones in the +Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.pubsub.credentials.scopes +OAuth2 scope for Spring Cloud GCP +Pub/Sub credentials +No +https://www.googleapis.com/auth/pubsub + + +spring.cloud.gcp.pubsub.subscriber.parallel-pull-count +The number of pull workers +No +The available number of processors + + +spring.cloud.gcp.pubsub.subscriber.max-ack-extension-period +The maximum period a message ack deadline will be extended, in seconds +No +0 + + +spring.cloud.gcp.pubsub.subscriber.pull-endpoint +The endpoint for synchronous pulling messages +No +pubsub.googleapis.com:443 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.total-timeout-seconds +TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. +The higher the total timeout, the more retries can be attempted. +No +0 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-retry-delay-second +InitialRetryDelay controls the delay before the first retry. +Subsequent retries will use this value adjusted according to the RetryDelayMultiplier. +No +0 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.retry-delay-multiplier +RetryDelayMultiplier controls the change in retry delay. +The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call. +No +1 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-retry-delay-seconds +MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier +can’t increase the retry delay higher than this amount. +No +0 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-attempts +MaxAttempts defines the maximum number of attempts to perform. +If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout. +No +0 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.jittered +Jitter determines if the delay time should be randomized. +No +true + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.initial-rpc-timeout-seconds +InitialRpcTimeout controls the timeout for the initial RPC. +Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier. +No +0 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.rpc-timeout-multiplier +RpcTimeoutMultiplier controls the change in RPC timeout. +The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call. +No +1 + + +spring.cloud.gcp.pubsub.[subscriber,publisher].retry.max-rpc-timeout-seconds +MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier +can’t increase the RPC timeout higher than this amount. +No +0 + + +spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-element-count +Maximum number of outstanding elements to keep in memory before enforcing flow control. +No +unlimited + + +spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.max-outstanding-request-bytes +Maximum number of outstanding bytes to keep in memory before enforcing flow control. +No +unlimited + + +spring.cloud.gcp.pubsub.[subscriber,publisher.batching].flow-control.limit-exceeded-behavior +The behavior when the specified limits are exceeded. +No +Block + + +spring.cloud.gcp.pubsub.publisher.batching.element-count-threshold +The element count threshold to use for batching. +No +unset (threshold does not apply) + + +spring.cloud.gcp.pubsub.publisher.batching.request-byte-threshold +The request byte threshold to use for batching. +No +unset (threshold does not apply) + + +spring.cloud.gcp.pubsub.publisher.batching.delay-threshold-seconds +The delay threshold to use for batching. +After this amount of time has elapsed (counting from the first element added), the elements will be wrapped up in a batch and sent. +No +unset (threshold does not apply) + + +spring.cloud.gcp.pubsub.publisher.batching.enabled +Enables batching. +No +false + + + + +
    +
    +Sample +A sample application is available. +
    +
    + +Spring Resources +Spring Resources are an abstraction for a number of low-level resources, such as file system files, classpath files, servlet context-relative files, etc. +Spring Cloud GCP adds a new resource type: a Google Cloud Storage (GCS) object. +A Spring Boot starter is provided to auto-configure the various Storage components. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-storage</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage' +} +This starter is also available from Spring Initializr through the GCP Storage entry. +
    +Google Cloud Storage +The Spring Resource Abstraction for Google Cloud Storage allows GCS objects to be accessed by their GCS URL using the @Value annotation: +@Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]") +private Resource gcsResource; +…​or the Spring application context +SpringApplication.run(...).getResource("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]"); +This creates a Resource object that can be used to read the object, among other possible operations. +It is also possible to write to a Resource, although a WriteableResource is required. +@Value("gs://[YOUR_GCS_BUCKET]/[GCS_FILE_NAME]") +private Resource gcsResource; +... +try (OutputStream os = ((WritableResource) gcsResource).getOutputStream()) { + os.write("foo".getBytes()); +} +To work with the Resource as a Google Cloud Storage resource, cast it to GoogleStorageResource. +If the resource path refers to an object on Google Cloud Storage (as opposed to a bucket), then the getBlob method can be called to obtain a Blob. +This type represents a GCS file, which has associated metadata, such as content-type, that can be set. +The createSignedUrl method can also be used to obtain signed URLs for GCS objects. +However, creating signed URLs requires that the resource was created using service account credentials. +The Spring Boot Starter for Google Cloud Storage auto-configures the Storage bean required by the spring-cloud-gcp-storage module, based on the CredentialsProvider provided by the Spring Boot GCP starter. +
    +Setting the Content Type +You can set the content-type of Google Cloud Storage files from their corresponding Resource objects: +((GoogleStorageResource)gcsResource).getBlob().toBuilder().setContentType("text/html").build().update(); +
    +
    +
    +Configuration +The Spring Boot Starter for Google Cloud Storage provides the following configuration options: + + + + + + + + +Name +Description +Required +Default value + + +spring.cloud.gcp.storage.enabled +Enables the GCP storage APIs. +No +true + + +spring.cloud.gcp.storage.auto-create-files +Creates files and buckets on Google Cloud Storage when writes are made to non-existent files +No +true + + +spring.cloud.gcp.storage.credentials.location +OAuth2 credentials for authenticating with the Google Cloud Storage API, if different from the ones in the Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.storage.credentials.encoded-key +Base64-encoded contents of OAuth2 account private key for authenticating with the Google Cloud Storage API, if different from the ones in the Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.storage.credentials.scopes +OAuth2 scope for Spring Cloud GCP Storage credentials +No +https://www.googleapis.com/auth/devstorage.read_write + + + + +
    +
    +Sample +A sample application and a codelab are available. +
    +
    + +Spring JDBC +Spring Cloud GCP adds integrations with +Spring JDBC so you can run your MySQL or PostgreSQL databases in Google Cloud SQL using Spring JDBC, or other libraries that depend on it like Spring Data JPA. +The Cloud SQL support is provided by Spring Cloud GCP in the form of two Spring Boot starters, one for MySQL and another one for PostgreSQL. +The role of the starters is to read configuration from properties and assume default settings so that user experience connecting to MySQL and PostgreSQL is as simple as possible. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId> +</dependency> +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-sql-postgresql</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-mysql' + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-sql-postgresql' +} +
    +Prerequisites +In order to use the Spring Boot Starters for Google Cloud SQL, the Google Cloud SQL API must be enabled in your GCP project. +To do that, go to the API library page of the Google Cloud Console, search for "Cloud SQL API", click the first result and enable the API. + +There are several similar "Cloud SQL" results. +You must access the "Google Cloud SQL API" one and enable the API from there. + +
    +
    +Spring Boot Starter for Google Cloud SQL +The Spring Boot Starters for Google Cloud SQL provide an auto-configured DataSource object. +Coupled with Spring JDBC, it provides a +JdbcTemplate object bean that allows for operations such as querying and modifying a database. +public List<Map<String, Object>> listUsers() { + return jdbcTemplate.queryForList("SELECT * FROM user;"); +} +You can rely on +Spring Boot data source auto-configuration to configure a DataSource bean. +In other words, properties like the SQL username, spring.datasource.username, and password, spring.datasource.password can be used. +There is also some configuration specific to Google Cloud SQL: + + + + + + + +Property name +Description +Default value + + +spring.cloud.gcp.sql.enabled +Enables or disables Cloud SQL auto configuration +true + + +spring.cloud.gcp.sql.database-name +Name of the database to connect to. + + + +spring.cloud.gcp.sql.instance-connection-name +A string containing a Google Cloud SQL instance’s project ID, region and name, each separated by a colon. +For example, my-project-id:my-region:my-instance-name. + + + +spring.cloud.gcp.sql.credentials.location +File system path to the Google OAuth2 credentials private key file. +Used to authenticate and authorize new connections to a Google Cloud SQL instance. +Default credentials provided by the Spring GCP Boot starter + + +spring.cloud.gcp.sql.credentials.encoded-key +Base64-encoded contents of OAuth2 account private key in JSON format. +Used to authenticate and authorize new connections to a Google Cloud SQL instance. +Default credentials provided by the Spring GCP Boot starter + + + + + +If you provide your own spring.datasource.url, it will be ignored, unless you disable Cloud SQL auto configuration with spring.cloud.gcp.sql.enabled=false. + +
    +<literal>DataSource</literal> creation flow +Based on the previous properties, the Spring Boot starter for Google Cloud SQL creates a CloudSqlJdbcInfoProvider object which is used to obtain an instance’s JDBC URL and driver class name. +If you provide your own CloudSqlJdbcInfoProvider bean, it is used instead and the properties related to building the JDBC URL or driver class are ignored. +The DataSourceProperties object provided by Spring Boot Autoconfigure is mutated in order to use the JDBC URL and driver class names provided by CloudSqlJdbcInfoProvider, unless those values were provided in the properties. +It is in the DataSourceProperties mutation step that the credentials factory is registered in a system property to be SqlCredentialFactory. +DataSource creation is delegated to +Spring Boot. +You can select the type of connection pool (e.g., Tomcat, HikariCP, etc.) by adding their dependency to the classpath. +Using the created DataSource in conjunction with Spring JDBC provides you with a fully configured and operational JdbcTemplate object that you can use to interact with your SQL database. +You can connect to your database with as little as a database and instance names. +
    +
    +Troubleshooting tips +
    +Connection issues +If you’re not able to connect to a database and see an endless loop of Connecting to Cloud SQL instance […​] on IP […​], it’s likely that exceptions are being thrown and logged at a level lower than your logger’s level. +This may be the case with HikariCP, if your logger is set to INFO or higher level. +To see what’s going on in the background, you should add a logback.xml file to your application resources folder, that looks like this: +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <include resource="org/springframework/boot/logging/logback/base.xml"/> + <logger name="com.zaxxer.hikari.pool" level="DEBUG"/> +</configuration> +
    +
    +Errors like <literal>c.g.cloud.sql.core.SslSocketFactory : Re-throwing cached exception due to attempt to refresh instance information too soon after error</literal> +If you see a lot of errors like this in a loop and can’t connect to your database, this is usually a symptom that something isn’t right with the permissions of your credentials or the Google Cloud SQL API is not enabled. +Verify that the Google Cloud SQL API is enabled in the Cloud Console and that your service account has the necessary IAM roles. +To find out what’s causing the issue, you can enable DEBUG logging level as mentioned above. +
    +
    +PostgreSQL: <literal>java.net.SocketException: already connected</literal> issue +We found this exception to be common if your Maven project’s parent is spring-boot version 1.5.x, or in any other circumstance that would cause the version of the org.postgresql:postgresql dependency to be an older one (e.g., 9.4.1212.jre7). +To fix this, re-declare the dependency in its correct version. +For example, in Maven: +<dependency> + <groupId>org.postgresql</groupId> + <artifactId>postgresql</artifactId> + <version>42.1.1</version> +</dependency> +
    +
    +
    +
    +Samples +Available sample applications and codelabs: + + +Spring Cloud GCP MySQL + + +Spring Cloud GCP PostgreSQL + + +Spring Data JPA with Spring Cloud GCP SQL + + +Codelab: Spring Pet Clinic using Cloud SQL + + +
    +
    + +Spring Integration +Spring Cloud GCP provides Spring Integration adapters that allow your applications to use Enterprise Integration Patterns backed up by Google Cloud Platform services. +
    +Channel Adapters for Cloud Pub/Sub +The channel adapters for Google Cloud Pub/Sub connect your Spring MessageChannels to Google Cloud Pub/Sub topics and subscriptions. +This enables messaging between different processes, applications or micro-services backed up by Google Cloud Pub/Sub. +The Spring Integration Channel Adapters for Google Cloud Pub/Sub are included in the spring-cloud-gcp-pubsub module. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-pubsub</artifactId> +</dependency> +<dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-core</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub' + compile group: 'org.springframework.integration', name: 'spring-integration-core' +} +
    +Inbound channel adapter +PubSubInboundChannelAdapter is the inbound channel adapter for GCP Pub/Sub that listens to a GCP Pub/Sub subscription for new messages. +It converts new messages to an internal Spring Message and then sends it to the bound output channel. +Google Pub/Sub treats message payloads as byte arrays. +So, by default, the inbound channel adapter will construct the Spring Message with byte[] as the payload. +However, you can change the desired payload type by setting the payloadType property of the PubSubInboundChannelAdapter. +The PubSubInboundChannelAdapter delegates the conversion to the desired payload type to the PubSubMessageConverter configured in the PubSubTemplate. +To use the inbound channel adapter, a PubSubInboundChannelAdapter must be provided and configured on the user application side. +@Bean +public MessageChannel pubsubInputChannel() { + return new PublishSubscribeChannel(); +} + +@Bean +public PubSubInboundChannelAdapter messageChannelAdapter( + @Qualifier("pubsubInputChannel") MessageChannel inputChannel, + SubscriberFactory subscriberFactory) { + PubSubInboundChannelAdapter adapter = + new PubSubInboundChannelAdapter(subscriberFactory, "subscriptionName"); + adapter.setOutputChannel(inputChannel); + adapter.setAckMode(AckMode.MANUAL); + + return adapter; +} +In the example, we first specify the MessageChannel where the adapter is going to write incoming messages to. +The MessageChannel implementation isn’t important here. +Depending on your use case, you might want to use a MessageChannel other than PublishSubscribeChannel. +Then, we declare a PubSubInboundChannelAdapter bean. +It requires the channel we just created and a SubscriberFactory, which creates Subscriber objects from the Google Cloud Java Client for Pub/Sub. +The Spring Boot starter for GCP Pub/Sub provides a configured SubscriberFactory. +The PubSubInboundChannelAdapter supports three acknowledgement modes, with AckMode.AUTO being the default value; +Automatic acking (AckMode.AUTO) +A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown. +If a RuntimeException is thrown while the message is processed, then the message is nacked. +Automatic acking OK (AckMode.AUTO_ACK) +A message is acked with GCP Pub/Sub if the adapter sent it to the channel and no exceptions were thrown. +If a RuntimeException is thrown while the message is processed, then the message is neither acked / nor nacked. +This is useful when using the subscription’s ack deadline timeout as a retry delivery backoff mechanism. +Manually acking (AckMode.MANUAL) +The adapter attaches a BasicAcknowledgeablePubsubMessage object to the Message headers. +Users can extract the BasicAcknowledgeablePubsubMessage using the GcpPubSubHeaders.ORIGINAL_MESSAGE key and use it to (n)ack a message. +@Bean +@ServiceActivator(inputChannel = "pubsubInputChannel") +public MessageHandler messageReceiver() { + return message -> { + LOGGER.info("Message arrived! Payload: " + new String((byte[]) message.getPayload())); + BasicAcknowledgeablePubsubMessage originalMessage = + message.getHeaders().get(GcpPubSubHeaders.ORIGINAL_MESSAGE, BasicAcknowledgeablePubsubMessage.class); + originalMessage.ack(); + }; +} +
    +
    +Outbound channel adapter +PubSubMessageHandler is the outbound channel adapter for GCP Pub/Sub that listens for new messages on a Spring MessageChannel. +It uses PubSubTemplate to post them to a GCP Pub/Sub topic. +To construct a Pub/Sub representation of the message, the outbound channel adapter needs to convert the Spring Message payload to a byte array representation expected by Pub/Sub. +It delegates this conversion to the PubSubTemplate. +To customize the conversion, you can specify a PubSubMessageConverter in the PubSubTemplate that should convert the Object payload and headers of the Spring Message to a PubsubMessage. +To use the outbound channel adapter, a PubSubMessageHandler bean must be provided and configured on the user application side. +@Bean +@ServiceActivator(inputChannel = "pubsubOutputChannel") +public MessageHandler messageSender(PubSubTemplate pubsubTemplate) { + return new PubSubMessageHandler(pubsubTemplate, "topicName"); +} +The provided PubSubTemplate contains all the necessary configuration to publish messages to a GCP Pub/Sub topic. +PubSubMessageHandler publishes messages asynchronously by default. +A publish timeout can be configured for synchronous publishing. +If none is provided, the adapter waits indefinitely for a response. +It is possible to set user-defined callbacks for the publish() call in PubSubMessageHandler through the setPublishFutureCallback() method. +These are useful to process the message ID, in case of success, or the error if any was thrown. +To override the default destination you can use the GcpPubSubHeaders.DESTINATION header. +@Autowired +private MessageChannel pubsubOutputChannel; + +public void handleMessage(Message<?> msg) throws MessagingException { + final Message<?> message = MessageBuilder + .withPayload(msg.getPayload()) + .setHeader(GcpPubSubHeaders.TOPIC, "customTopic").build(); + pubsubOutputChannel.send(message); +} +It is also possible to set an SpEL expression for the topic with the setTopicExpression() or setTopicExpressionString() methods. +
    +
    +Header mapping +These channel adapters contain header mappers that allow you to map, or filter out, headers from Spring to Google Cloud Pub/Sub messages, and vice-versa. +By default, the inbound channel adapter maps every header on the Google Cloud Pub/Sub messages to the Spring messages produced by the adapter. +The outbound channel adapter maps every header from Spring messages into Google Cloud Pub/Sub ones, except the ones added by Spring, like headers with key "id", "timestamp" and "gcp_pubsub_acknowledgement". +In the process, the outbound mapper also converts the value of the headers into string. +Each adapter declares a setHeaderMapper() method to let you further customize which headers you want to map from Spring to Google Cloud Pub/Sub, and vice-versa. +For example, to filter out headers "foo", "bar" and all headers starting with the prefix "prefix_", you can use setHeaderMapper() along with the PubSubHeaderMapper implementation provided by this module. +PubSubMessageHandler adapter = ... +... +PubSubHeaderMapper headerMapper = new PubSubHeaderMapper(); +headerMapper.setOutboundHeaderPatterns("!foo", "!bar", "!prefix_*", "*"); +adapter.setHeaderMapper(headerMapper); + +The order in which the patterns are declared in PubSubHeaderMapper.setOutboundHeaderPatterns() and PubSubHeaderMapper.setInboundHeaderPatterns() matters. +The first patterns have precedence over the following ones. + +In the previous example, the "*" pattern means every header is mapped. +However, because it comes last in the list, the previous patterns take precedence. +
    +
    +
    +Sample +Available examples: + + +sender and receiver sample application + + +JSON payloads sample application + + +codelab + + +
    +
    +Channel Adapters for Google Cloud Storage +The channel adapters for Google Cloud Storage allow you to read and write files to Google Cloud Storage through MessageChannels. +Spring Cloud GCP provides two inbound adapters, GcsInboundFileSynchronizingMessageSource and GcsStreamingMessageSource, and one outbound adapter, GcsMessageHandler. +The Spring Integration Channel Adapters for Google Cloud Storage are included in the spring-cloud-gcp-storage module. +To use the Storage portion of Spring Integration for Spring Cloud GCP, you must also provide the spring-integration-file dependency, since it isn’t pulled transitively. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-storage</artifactId> +</dependency> +<dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-file</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-storage' + compile group: 'org.springframework.integration', name: 'spring-integration-file' +} +
    +Inbound channel adapter +The Google Cloud Storage inbound channel adapter polls a Google Cloud Storage bucket for new files and sends each of them in a Message payload to the MessageChannel specified in the @InboundChannelAdapter annotation. +The files are temporarily stored in a folder in the local file system. +Here is an example of how to configure a Google Cloud Storage inbound channel adapter. +@Bean +@InboundChannelAdapter(channel = "new-file-channel", poller = @Poller(fixedDelay = "5000")) +public MessageSource<File> synchronizerAdapter(Storage gcs) { + GcsInboundFileSynchronizer synchronizer = new GcsInboundFileSynchronizer(gcs); + synchronizer.setRemoteDirectory("your-gcs-bucket"); + + GcsInboundFileSynchronizingMessageSource synchAdapter = + new GcsInboundFileSynchronizingMessageSource(synchronizer); + synchAdapter.setLocalDirectory(new File("local-directory")); + + return synchAdapter; +} +
    +
    +Inbound streaming channel adapter +The inbound streaming channel adapter is similar to the normal inbound channel adapter, except it does not require files to be stored in the file system. +Here is an example of how to configure a Google Cloud Storage inbound streaming channel adapter. +@Bean +@InboundChannelAdapter(channel = "streaming-channel", poller = @Poller(fixedDelay = "5000")) +public MessageSource<InputStream> streamingAdapter(Storage gcs) { + GcsStreamingMessageSource adapter = + new GcsStreamingMessageSource(new GcsRemoteFileTemplate(new GcsSessionFactory(gcs))); + adapter.setRemoteDirectory("your-gcs-bucket"); + return adapter; +} +
    +
    +Outbound channel adapter +The outbound channel adapter allows files to be written to Google Cloud Storage. +When it receives a Message containing a payload of type File, it writes that file to the Google Cloud Storage bucket specified in the adapter. +Here is an example of how to configure a Google Cloud Storage outbound channel adapter. +@Bean +@ServiceActivator(inputChannel = "writeFiles") +public MessageHandler outboundChannelAdapter(Storage gcs) { + GcsMessageHandler outboundChannelAdapter = new GcsMessageHandler(new GcsSessionFactory(gcs)); + outboundChannelAdapter.setRemoteDirectoryExpression(new ValueExpression<>("your-gcs-bucket")); + + return outboundChannelAdapter; +} +
    +
    +
    +Sample +A sample application is available. +
    +
    + +Spring Cloud Stream +Spring Cloud GCP provides a Spring Cloud Stream binder to Google Cloud Pub/Sub. +The provided binder relies on the Spring Integration Channel Adapters for Google Cloud Pub/Sub. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-pubsub-stream-binder</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-pubsub-stream-binder' +} +
    +Overview +This binder binds producers to Google Cloud Pub/Sub topics and consumers to subscriptions. + +Partitioning is currently not supported by this binder. + +
    +
    +Configuration +You can configure the Spring Cloud Stream Binder for Google Cloud Pub/Sub to automatically generate the underlying resources, like the Google Cloud Pub/Sub topics and subscriptions for producers and consumers. +For that, you can use the spring.cloud.stream.gcp.pubsub.bindings.<channelName>.<consumer|producer>.auto-create-resources property, which is turned ON by default. +Starting with version 1.1, these and other binder properties can be configured globally for all the bindings, e.g. spring.cloud.stream.gcp.pubsub.default.consumer.auto-create-resources. +If you are using Pub/Sub auto-configuration from the Spring Cloud GCP Pub/Sub Starter, you should refer to the configuration section for other Pub/Sub parameters. + +To use this binder with a running emulator, configure its host and port via spring.cloud.gcp.pubsub.emulator-host. + +
    +Producer Destination Configuration +If automatic resource creation is turned ON and the topic corresponding to the destination name does not exist, it will be created. +For example, for the following configuration, a topic called myEvents would be created. + +application.properties + +spring.cloud.stream.bindings.events.destination=myEvents +spring.cloud.stream.gcp.pubsub.bindings.events.producer.auto-create-resources=true + + +
    +
    +Consumer Destination Configuration +If automatic resource creation is turned ON and the subscription and/or the topic do not exist for a consumer, a subscription and potentially a topic will be created. +The topic name will be the same as the destination name, and the subscription name will be the destination name followed by the consumer group name. +Regardless of the auto-create-resources setting, if the consumer group is not specified, an anonymous one will be created with the name anonymous.<destinationName>.<randomUUID>. +Then when the binder shuts down, all Pub/Sub subscriptions created for anonymous consumer groups will be automatically cleaned up. +For example, for the following configuration, a topic named myEvents and a subscription called myEvents.counsumerGroup1 would be created. +If the consumer group is not specified, a subscription called anonymous.myEvents.a6d83782-c5a3-4861-ac38-e6e2af15a7be would be created and later cleaned up. + +If you are manually creating Pub/Sub subscriptions for consumers, make sure that they follow the naming convention of <destinationName>.<consumerGroup>. + + +application.properties + +spring.cloud.stream.bindings.events.destination=myEvents +spring.cloud.stream.gcp.pubsub.bindings.events.consumer.auto-create-resources=true + +# specify consumer group, and avoid anonymous consumer group generation +spring.cloud.stream.bindings.events.group=consumerGroup1 + + +
    +
    +
    +Sample +A sample application is available. +
    +
    + +Spring Cloud Sleuth +Spring Cloud Sleuth is an instrumentation framework for Spring Boot applications. +It captures trace information and can forward traces to services like Zipkin for storage and analysis. +Google Cloud Platform provides its own managed distributed tracing service called Stackdriver Trace. +Instead of running and maintaining your own Zipkin instance and storage, you can use Stackdriver Trace to store traces, view trace details, generate latency distributions graphs, and generate performance regression reports. +This Spring Cloud GCP starter can forward Spring Cloud Sleuth traces to Stackdriver Trace without an intermediary Zipkin server. +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-trace</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-trace' +} +You must enable Stackdriver Trace API from the Google Cloud Console in order to capture traces. +Navigate to the Stackdriver Trace API for your project and make sure it’s enabled. + +If you are already using a Zipkin server capturing trace information from multiple platform/frameworks, you can also use a Stackdriver Zipkin proxy to forward those traces to Stackdriver Trace without modifying existing applications. + +
    +Tracing +Spring Cloud Sleuth uses the Brave tracer to generate traces. +This integration enables Brave to use the StackdriverTracePropagation propagation. +A propagation is responsible for extracting trace context from an entity (e.g., an HTTP servlet request) and injecting trace context into an entity. +A canonical example of the propagation usage is a web server that receives an HTTP request, which triggers other HTTP requests from the server before returning an HTTP response to the original caller. +In the case of StackdriverTracePropagation, first it looks for trace context in the x-cloud-trace-context key (e.g., an HTTP request header). +The value of the x-cloud-trace-context key can be formatted in three different ways: + + +x-cloud-trace-context: TRACE_ID + + +x-cloud-trace-context: TRACE_ID/SPAN_ID + + +x-cloud-trace-context: TRACE_ID/SPAN_ID;o=TRACE_TRUE + + +TRACE_ID is a 32-character hexadecimal value that encodes a 128-bit number. +SPAN_ID is an unsigned long. +Since Stackdriver Trace doesn’t support span joins, a new span ID is always generated, regardless of the one specified in x-cloud-trace-context. +TRACE_TRUE can either be 0 if the entity should be untraced, or 1 if it should be traced. +This field forces the decision of whether or not to trace the request; if omitted then the decision is deferred to the sampler. +If a x-cloud-trace-context key isn’t found, StackdriverTracePropagation falls back to tracing with the X-B3 headers. +
    +
    +Spring Boot Starter for Stackdriver Trace +Spring Boot Starter for Stackdriver Trace uses Spring Cloud Sleuth and auto-configures a StackdriverSender that sends the Sleuth’s trace information to Stackdriver Trace. +All configurations are optional: + + + + + + + + +Name +Description +Required +Default value + + +spring.cloud.gcp.trace.enabled +Auto-configure Spring Cloud Sleuth to send traces to Stackdriver Trace. +No +true + + +spring.cloud.gcp.trace.project-id +Overrides the project ID from the Spring Cloud GCP Module +No + + + +spring.cloud.gcp.trace.credentials.location +Overrides the credentials location from the Spring Cloud GCP Module +No + + + +spring.cloud.gcp.trace.credentials.encoded-key +Overrides the credentials encoded key from the Spring Cloud GCP Module +No + + + +spring.cloud.gcp.trace.credentials.scopes +Overrides the credentials scopes from the Spring Cloud GCP Module +No + + + +spring.cloud.gcp.trace.num-executor-threads +Number of threads used by the Trace executor +No +4 + + +spring.cloud.gcp.trace.authority +HTTP/2 authority the channel claims to be connecting to. +No + + + +spring.cloud.gcp.trace.compression +Name of the compression to use in Trace calls +No + + + +spring.cloud.gcp.trace.deadline-ms +Call deadline in milliseconds +No + + + +spring.cloud.gcp.trace.max-inbound-size +Maximum size for inbound messages +No + + + +spring.cloud.gcp.trace.max-outbound-size +Maximum size for outbound messages +No + + + +spring.cloud.gcp.trace.wait-for-ready +Waits for the channel to be ready in case of a transient failure +No +false + + +spring.cloud.gcp.trace.messageTimeout +Timeout in seconds before pending spans will be sent in batches to GCP Stackdriver Trace. Added for forward compatibility. +No +spring.zipkin.messageTimeout + + + + +You can use core Spring Cloud Sleuth properties to control Sleuth’s sampling rate, etc. +Read Sleuth documentation for more information on Sleuth configurations. +For example, when you are testing to see the traces are going through, you can set the sampling rate to 100%. +spring.sleuth.sampler.probability=1 # Send 100% of the request traces to Stackdriver. +spring.sleuth.web.skipPattern=(^cleanup.*|.+favicon.*) # Ignore some URL paths. +Spring Cloud GCP Trace does override some Sleuth configurations: + + +Always uses 128-bit Trace IDs. +This is required by Stackdriver Trace. + + +Does not use Span joins. +Span joins will share the span ID between the client and server Spans. +Stackdriver requires that every Span ID within a Trace to be unique, so Span joins are not supported. + + +Uses StackdriverHttpClientParser and StackdriverHttpServerParser by default to populate Stackdriver related fields. + + +
    +
    +Overriding the auto-configuration +Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. +In order to get this to work, every tracing system needs to have a Reporter<Span> and Sender. +If you want to override the provided beans you need to give them a specific name. +To do this you can use respectively StackdriverTraceAutoConfiguration.REPORTER_BEAN_NAME and StackdriverTraceAutoConfiguration.SENDER_BEAN_NAME. +
    +
    +Integration with Logging +Integration with Stackdriver Logging is available through the Stackdriver Logging Support. +If the Trace integration is used together with the Logging one, the request logs will be associated to the corresponding traces. +The trace logs can be viewed by going to the Google Cloud Console Trace List, selecting a trace and pressing the Logs → View link in the Details section. +
    +
    +Sample +A sample application and a codelab are available. +
    +
    + +Stackdriver Logging +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-logging</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-logging' +} +Stackdriver Logging is the managed logging service provided by Google Cloud Platform. +This module provides support for associating a web request trace ID with the corresponding log entries. +It does so by retrieving the X-B3-TraceId value from the Mapped Diagnostic Context (MDC), which is set by Spring Cloud Sleuth. +If Spring Cloud Sleuth isn’t used, the configured TraceIdExtractor extracts the desired header value and sets it as the log entry’s trace ID. +This allows grouping of log messages by request, for example, in the Google Cloud Console Logs viewer. + +Due to the way logging is set up, the GCP project ID and credentials defined in application.properties are ignored. +Instead, you should set the GOOGLE_CLOUD_PROJECT and GOOGLE_APPLICATION_CREDENTIALS environment variables to the project ID and credentials private key location, respectively. +You can do this easily if you’re using the Google Cloud SDK, using the gcloud config set project [YOUR_PROJECT_ID] and gcloud auth application-default login commands, respectively. + +
    +Web MVC Interceptor +For use in Web MVC-based applications, TraceIdLoggingWebMvcInterceptor is provided that extracts the request trace ID from an HTTP request using a TraceIdExtractor and stores it in a thread-local, which can then be used in a logging appender to add the trace ID metadata to log messages. + +If Spring Cloud GCP Trace is enabled, the logging module disables itself and delegates log correlation to Spring Cloud Sleuth. + +LoggingWebMvcConfigurer configuration class is also provided to help register the TraceIdLoggingWebMvcInterceptor in Spring MVC applications. +Applications hosted on the Google Cloud Platform include trace IDs under the x-cloud-trace-context header, which will be included in log entries. +However, if Sleuth is used the trace ID will be picked up from the MDC. +
    +
    +Logback Support +Currently, only Logback is supported and there are 2 possibilities to log to Stackdriver via this library with Logback: via direct API calls and through JSON-formatted console logs. +
    +Log via API +A Stackdriver appender is available using org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml. +This appender builds a Stackdriver Logging log entry from a JUL or Logback log entry, adds a trace ID to it and sends it to Stackdriver Logging. +STACKDRIVER_LOG_NAME and STACKDRIVER_LOG_FLUSH_LEVEL environment variables can be used to customize the STACKDRIVER appender. +Your configuration may then look like this: +<configuration> + <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-appender.xml" /> + + <root level="INFO"> + <appender-ref ref="STACKDRIVER" /> + </root> +</configuration> +If you want to have more control over the log output, you can further configure the appender. +The following properties are available: + + + + + + + +Property +Default Value +Description + + + + +log +spring.log +The Stackdriver Log name. +This can also be set via the STACKDRIVER_LOG_NAME environmental variable. + + +flushLevel +WARN +If a log entry with this level is encountered, trigger a flush of locally buffered log to Stackdriver Logging. +This can also be set via the STACKDRIVER_LOG_FLUSH_LEVEL environmental variable. + + + + +
    +
    +Log via Console +For Logback, a org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml file is made available for import to make it easier to configure the JSON Logback appender. +Your configuration may then look something like this: +<configuration> + <include resource="org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml" /> + + <root level="INFO"> + <appender-ref ref="CONSOLE_JSON" /> + </root> +</configuration> +If your application is running on Google Kubernetes Engine, Google Compute Engine or Google App Engine Flexible, your console logging is automatically saved to Google Stackdriver Logging. +Therefore, you can just include org/springframework/cloud/gcp/autoconfigure/logging/logback-json-appender.xml in your logging configuration, which logs JSON entries to the console. +The trace id will be set correctly. +If you want to have more control over the log output, you can further configure the appender. +The following properties are available: + + + + + + + +Property +Default Value +Description + + + + +projectId +If not set, default value is determined in the following order: + + +SPRING_CLOUD_GCP_LOGGING_PROJECT_ID Environmental Variable. + + +Value of DefaultGcpProjectIdProvider.getProjectId() + + +This is used to generate fully qualified Stackdriver Trace ID format: projects/[PROJECT-ID]/traces/[TRACE-ID]. +This format is required to correlate trace between Stackdriver Trace and Stackdriver Logging. +If projectId is not set and cannot be determined, then it’ll log traceId without the fully qualified format. + + +includeTraceId +true +Should the traceId be included + + +includeSpanId +true +Should the spanId be included + + +includeLevel +true +Should the severity be included + + +includeThreadName +true +Should the thread name be included + + +includeMDC +true +Should all MDC properties be included. +The MDC properties X-B3-TraceId, X-B3-SpanId and X-Span-Export provided by Spring Sleuth will get excluded as they get handled separately + + +includeLoggerName +true +Should the name of the logger be included + + +includeFormattedMessage +true +Should the formatted log message be included. + + +includeExceptionInMessage +true +Should the stacktrace be appended to the formatted log message. +This setting is only evaluated if includeFormattedMessage is true + + +includeContextName +true +Should the logging context be included + + +includeMessage +false +Should the log message with blank placeholders be included + + +includeException +false +Should the stacktrace be included as a own field + + + + +This is an example of such an Logback configuration: +<configuration > + <property name="projectId" value="${projectId:-${GOOGLE_CLOUD_PROJECT}}"/> + + <appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender"> + <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> + <layout class="org.springframework.cloud.gcp.logging.StackdriverJsonLayout"> + <projectId>${projectId}</projectId> + + <!--<includeTraceId>true</includeTraceId>--> + <!--<includeSpanId>true</includeSpanId>--> + <!--<includeLevel>true</includeLevel>--> + <!--<includeThreadName>true</includeThreadName>--> + <!--<includeMDC>true</includeMDC>--> + <!--<includeLoggerName>true</includeLoggerName>--> + <!--<includeFormattedMessage>true</includeFormattedMessage>--> + <!--<includeExceptionInMessage>true</includeExceptionInMessage>--> + <!--<includeContextName>true</includeContextName>--> + <!--<includeMessage>false</includeMessage>--> + <!--<includeException>false</includeException>--> + </layout> + </encoder> + </appender> +</configuration> +
    +
    +
    +Sample +A Sample Spring Boot Application is provided to show how to use the Cloud logging starter. +
    +
    + +Spring Cloud Config +Spring Cloud GCP makes it possible to use the Google Runtime Configuration API as a Spring Cloud Config server to remotely store your application configuration data. +The Spring Cloud GCP Config support is provided via its own Spring Boot starter. +It enables the use of the Google Runtime Configuration API as a source for Spring Boot configuration properties. + +The Google Cloud Runtime Configuration service is in beta status. + +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-config</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-config' +} +
    +Configuration +The following parameters are configurable in Spring Cloud GCP Config: + + + + + + + + +Name +Description +Required +Default value + + +spring.cloud.gcp.config.enabled +Enables the Config client +No +false + + +spring.cloud.gcp.config.name +Name of your application +No +Value of the spring.application.name property. +If none, application + + +spring.cloud.gcp.config.profile +Active profile +No +Value of the spring.profiles.active property. +If more than a single profile, last one is chosen + + +spring.cloud.gcp.config.timeout-millis +Timeout in milliseconds for connecting to the Google Runtime Configuration API +No +60000 + + +spring.cloud.gcp.config.project-id +GCP project ID where the Google Runtime Configuration API is hosted +No + + + +spring.cloud.gcp.config.credentials.location +OAuth2 credentials for authenticating with the Google Runtime Configuration API +No + + + +spring.cloud.gcp.config.credentials.encoded-key +Base64-encoded OAuth2 credentials for authenticating with the Google Runtime Configuration API +No + + + +spring.cloud.gcp.config.credentials.scopes +OAuth2 scope for Spring Cloud GCP Config credentials +No +https://www.googleapis.com/auth/cloudruntimeconfig + + + + + +These properties should be specified in a bootstrap.yml/bootstrap.properties file, rather than the usual applications.yml/application.properties. + + +Core properties, as described in Spring Cloud GCP Core Module, do not apply to Spring Cloud GCP Config. + +
    +
    +Quick start + + +Create a configuration in the Google Runtime Configuration API that is called ${spring.application.name}_${spring.profiles.active}. +In other words, if spring.application.name is myapp and spring.profiles.active is prod, the configuration should be called myapp_prod. +In order to do that, you should have the Google Cloud SDK installed, own a Google Cloud Project and run the following command: + + +gcloud init # if this is your first Google Cloud SDK run. +gcloud beta runtime-config configs create myapp_prod +gcloud beta runtime-config configs variables set myapp.queue-size 25 --config-name myapp_prod + + +Configure your bootstrap.properties file with your application’s configuration data: +spring.application.name=myapp +spring.profiles.active=prod + + +Add the @ConfigurationProperties annotation to a Spring-managed bean: +@Component +@ConfigurationProperties("myapp") +public class SampleConfig { + + private int queueSize; + + public int getQueueSize() { + return this.queueSize; + } + + public void setQueueSize(int queueSize) { + this.queueSize = queueSize; + } +} + + +When your Spring application starts, the queueSize field value will be set to 25 for the above SampleConfig bean. +
    +
    +Refreshing the configuration at runtime +Spring Cloud provides support to have configuration parameters be reloadable with the POST request to /actuator/refresh endpoint. + + +Add the Spring Boot Actuator dependency: + + +Maven coordinates: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-actuator</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator' +} + + +Add @RefreshScope to your Spring configuration class to have parameters be reloadable at runtime. + + +Add management.endpoints.web.exposure.include=refresh to your application.properties to allow unrestricted access to /actuator/refresh. + + +Update a property with gcloud: +$ gcloud beta runtime-config configs variables set \ + myapp.queue_size 200 \ + --config-name myapp_prod + + +Send a POST request to the refresh endpoint: +$ curl -XPOST https://myapp.host.com/actuator/refresh + + +
    +
    +Sample +A sample application and a codelab are available. +
    +
    + +Spring Data Cloud Spanner +Spring Data is an abstraction for storing and retrieving POJOs in numerous storage technologies. +Spring Cloud GCP adds Spring Data support for Google Cloud Spanner. +Maven coordinates for this module only, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-data-spanner</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-spanner' +} +We provide a Spring Boot Starter for Spring Data Spanner, with which you can leverage our recommended auto-configuration setup. +To use the starter, see the coordinates see below. +Maven: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-data-spanner</artifactId> +</dependency> +Gradle: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-spanner' +} +This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Spanner libraries as well. +
    +Configuration +To setup Spring Data Cloud Spanner, you have to configure the following: + + +Setup the connection details to Google Cloud Spanner. + + +Enable Spring Data Repositories (optional). + + +
    +Cloud Spanner settings +You can the use Spring Boot Starter for Spring Data Spanner to autoconfigure Google Cloud Spanner in your Spring application. +It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project. +The following configuration options are available: + + + + + + + + +Name +Description +Required +Default value + + +spring.cloud.gcp.spanner.instance-id +Cloud Spanner instance to use +Yes + + + +spring.cloud.gcp.spanner.database +Cloud Spanner database to use +Yes + + + +spring.cloud.gcp.spanner.project-id +GCP project ID where the Google Cloud Spanner API is hosted, if different from the one in the Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.spanner.credentials.location +OAuth2 credentials for authenticating with the +Google Cloud Spanner API, if different from the ones in the +Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.spanner.credentials.encoded-key +Base64-encoded OAuth2 credentials for authenticating with the +Google Cloud Spanner API, if different from the ones in the +Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.spanner.credentials.scopes +OAuth2 scope for Spring Cloud GCP +Cloud Spanner credentials +No +https://www.googleapis.com/auth/spanner.data + + +spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade +If true, then schema statements generated by SpannerSchemaUtils for tables with interleaved parent-child relationships will be "ON DELETE CASCADE". +The schema for the tables will be "ON DELETE NO ACTION" if false. +No +true + + +spring.cloud.gcp.spanner.numRpcChannels +Number of gRPC channels used to connect to Cloud Spanner +No +4 - Determined by Cloud Spanner client library + + +spring.cloud.gcp.spanner.prefetchChunks +Number of chunks prefetched by Cloud Spanner for read and query +No +4 - Determined by Cloud Spanner client library + + +spring.cloud.gcp.spanner.minSessions +Minimum number of sessions maintained in the session pool +No +0 - Determined by Cloud Spanner client library + + +spring.cloud.gcp.spanner.maxSessions +Maximum number of sessions session pool can have +No +400 - Determined by Cloud Spanner client library + + +spring.cloud.gcp.spanner.maxIdleSessions +Maximum number of idle sessions session pool will maintain +No +0 - Determined by Cloud Spanner client library + + +spring.cloud.gcp.spanner.writeSessionsFraction +Fraction of sessions to be kept prepared for write transactions +No +0.2 - Determined by Cloud Spanner client library + + +spring.cloud.gcp.spanner.keepAliveIntervalMinutes +How long to keep idle sessions alive +No +30 - Determined by Cloud Spanner client library + + + + +
    +
    +Repository settings +Spring Data Repositories can be configured via the @EnableSpannerRepositories annotation on your main @Configuration class. +With our Spring Boot Starter for Spring Data Cloud Spanner, @EnableSpannerRepositories is automatically added. +It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by @EnableSpannerRepositories. +
    +
    +Autoconfiguration +Our Spring Boot autoconfiguration creates the following beans available in the Spring application context: + + +an instance of SpannerTemplate + + +an instance of SpannerDatabaseAdminTemplate for generating table schemas from object hierarchies and creating and deleting tables and databases + + +an instance of all user-defined repositories extending SpannerRepository, CrudRepository, PagingAndSortingRepository, when repositories are enabled + + +an instance of DatabaseClient from the Google Cloud Java Client for Spanner, for convenience and lower level API access + + +
    +
    +
    +Object Mapping +Spring Data Cloud Spanner allows you to map domain POJOs to Cloud Spanner tables via annotations: +@Table(name = "traders") +public class Trader { + + @PrimaryKey + @Column(name = "trader_id") + String traderId; + + String firstName; + + String lastName; + + @NotMapped + Double temporaryNumber; +} +Spring Data Cloud Spanner will ignore any property annotated with @NotMapped. +These properties will not be written to or read from Spanner. +
    +Constructors +Simple constructors are supported on POJOs. +The constructor arguments can be a subset of the persistent properties. +Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument. +Arguments that are not directly set to properties are not supported. +@Table(name = "traders") +public class Trader { + @PrimaryKey + @Column(name = "trader_id") + String traderId; + + String firstName; + + String lastName; + + @NotMapped + Double temporaryNumber; + + public Trader(String traderId, String firstName) { + this.traderId = traderId; + this.firstName = firstName; + } +} +
    +
    +Table +The @Table annotation can provide the name of the Cloud Spanner table that stores instances of the annotated class, one per row. +This annotation is optional, and if not given, the name of the table is inferred from the class name with the first character uncapitalized. +
    +SpEL expressions for table names +In some cases, you might want the @Table table name to be determined dynamically. +To do that, you can use Spring Expression Language. +For example: +@Table(name = "trades_#{tableNameSuffix}") +public class Trade { + // ... +} +The table name will be resolved only if the tableNameSuffix value/bean in the Spring application context is defined. +For example, if tableNameSuffix has the value "123", the table name will resolve to trades_123. +
    +
    +
    +Primary Keys +For a simple table, you may only have a primary key consisting of a single column. +Even in that case, the @PrimaryKey annotation is required. +@PrimaryKey identifies the one or more ID properties corresponding to the primary key. +Spanner has first class support for composite primary keys of multiple columns. +You have to annotate all of your POJO’s fields that the primary key consists of with @PrimaryKey as below: +@Table(name = "trades") +public class Trade { + @PrimaryKey(keyOrder = 2) + @Column(name = "trade_id") + private String tradeId; + + @PrimaryKey(keyOrder = 1) + @Column(name = "trader_id") + private String traderId; + + private String action; + + private Double price; + + private Double shares; + + private String symbol; +} +The keyOrder parameter of @PrimaryKey identifies the properties corresponding to the primary key columns in order, starting with 1 and increasing consecutively. +Order is important and must reflect the order defined in the Cloud Spanner schema. +In our example the DDL to create the table and its primary key is as follows: +CREATE TABLE trades ( + trader_id STRING(MAX), + trade_id STRING(MAX), + action STRING(15), + symbol STRING(10), + price FLOAT64, + shares FLOAT64 +) PRIMARY KEY (trader_id, trade_id) +Spanner does not have automatic ID generation. +For most use-cases, sequential IDs should be used with caution to avoid creating data hotspots in the system. +Read Spanner Primary Keys documentation for a better understanding of primary keys and recommended practices. +
    +
    +Columns +All accessible properties on POJOs are automatically recognized as a Cloud Spanner column. +Column naming is generated by the PropertyNameFieldNamingStrategy by default defined on the SpannerMappingContext bean. +The @Column annotation optionally provides a different column name than that of the property and some other settings: + + +name is the optional name of the column + + +spannerTypeMaxLength specifies for STRING and BYTES columns the maximum length. +This setting is only used when generating DDL schema statements based on domain types. + + +nullable specifies if the column is created as NOT NULL. +This setting is only used when generating DDL schema statements based on domain types. + + +spannerType is the Cloud Spanner column type you can optionally specify. +If this is not specified then a compatible column type is inferred from the Java property type. + + +spannerCommitTimestamp is a boolean specifying if this property corresponds to an auto-populated commit timestamp column. +Any value set in this property will be ignored when writing to Cloud Spanner. + + +
    +
    +Embedded Objects +If an object of type B is embedded as a property of A, then the columns of B will be saved in the same Cloud Spanner table as those of A. +If B has primary key columns, those columns will be included in the primary key of A. B can also have embedded properties. +Embedding allows reuse of columns between multiple entities, and can be useful for implementing parent-child situations, because Cloud Spanner requires child tables to include the key columns of their parents. +For example: +class X { + @PrimaryKey + String grandParentId; + + long age; +} + +class A { + @PrimaryKey + @Embedded + X grandParent; + + @PrimaryKey(keyOrder = 2) + String parentId; + + String value; +} + +@Table(name = "items") +class B { + @PrimaryKey + @Embedded + A parent; + + @PrimaryKey(keyOrder = 2) + String id; + + @Column(name = "child_value") + String value; +} +Entities of B can be stored in a table defined as: +CREATE TABLE items ( + grandParentId STRING(MAX), + parentId STRING(MAX), + id STRING(MAX), + value STRING(MAX), + child_value STRING(MAX), + age INT64 +) PRIMARY KEY (grandParentId, parentId, id) +Note that embedded properties' column names must all be unique. +
    +
    +Relationships +Spring Data Cloud Spanner supports parent-child relationships using the Cloud Spanner parent-child interleaved table mechanism. +Cloud Spanner interleaved tables enforce the one-to-many relationship and provide efficient queries and operations on entities of a single domain parent entity. +These relationships can be up to 7 levels deep. +Cloud Spanner also provides automatic cascading delete or enforces the deletion of child entities before parents. +While one-to-one and many-to-many relationships can be implemented in Cloud Spanner and Spring Data Cloud Spanner using constructs of interleaved parent-child tables, only the parent-child relationship is natively supported. +Cloud Spanner does not support the foreign key constraint, though the parent-child key constraint enforces a similar requirement when used with interleaved tables. +For example, the following Java entities: +@Table(name = "Singers") +class Singer { + @PrimaryKey + long SingerId; + + String FirstName; + + String LastName; + + byte[] SingerInfo; + + @Interleaved + List<Album> albums; +} + +@Table(name = "Albums") +class Album { + @PrimaryKey + long SingerId; + + @PrimaryKey(keyOrder = 2) + long AlbumId; + + String AlbumTitle; +} +These classes can correspond to an existing pair of interleaved tables. +The @Interleaved annotation may be applied to Collection properties and the inner type is resolved as the child entity type. +The schema needed to create them can also be generated using the SpannerSchemaUtils and executed using the SpannerDatabaseAdminTemplate: +@Autowired +SpannerSchemaUtils schemaUtils; + +@Autowired +SpannerDatabaseAdminTemplate databaseAdmin; +... + +// Get the create statmenets for all tables in the table structure rooted at Singer +List<String> createStrings = this.schemaUtils.getCreateTableDdlStringsForInterleavedHierarchy(Singer.class); + +// Create the tables and also create the database if necessary +this.databaseAdmin.executeDdlStrings(createStrings, true); +The createStrings list contains table schema statements using column names and types compatible with the provided Java type and any resolved child relationship types contained within based on the configured custom converters. +CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX), +) PRIMARY KEY (SingerId); + +CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX), +) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE; +The ON DELETE CASCADE clause indicates that Cloud Spanner will delete all Albums of a singer if the Singer is deleted. +The alternative is ON DELETE NO ACTION, where a Singer cannot be deleted until all of its Albums have already been deleted. +When using SpannerSchemaUtils to generate the schema strings, the spring.cloud.gcp.spanner.createInterleavedTableDdlOnDeleteCascade boolean setting determines if these schema are generated as ON DELETE CASCADE for true and ON DELETE NO ACTION for false. +Cloud Spanner restricts these relationships to 7 child layers. +A table may have multiple child tables. +On updating or inserting an object to Cloud Spanner, all of its referenced children objects are also updated or inserted in the same request, respectively. +On read, all of the interleaved child rows are also all read. +
    +
    +Supported Types +Spring Data Cloud Spanner natively supports the following types for regular fields but also utilizes custom converters (detailed in following sections) and dozens of pre-defined Spring Data custom converters to handle other common Java types. +Natively supported types: + + +com.google.cloud.ByteArray + + +com.google.cloud.Date + + +com.google.cloud.Timestamp + + +java.lang.Boolean, boolean + + +java.lang.Double, double + + +java.lang.Long, long + + +java.lang.Integer, int + + +java.lang.String + + +double[] + + +long[] + + +boolean[] + + +java.util.Date + + +java.util.Instant + + +java.sql.Date + + +
    +
    +Lists +Spanner supports ARRAY types for columns. +ARRAY columns are mapped to List fields in POJOS. +Example: +List<Double> curve; +The types inside the lists can be any singular property type. +
    +
    +Lists of Structs +Cloud Spanner queries can construct STRUCT values that appear as columns in the result. +Cloud Spanner requires STRUCT values appear in ARRAYs at the root level: SELECT ARRAY(SELECT STRUCT(1 as val1, 2 as val2)) as pair FROM Users. +Spring Data Cloud Spanner will attempt to read the column STRUCT values into a property that is an Iterable of an entity type compatible with the schema of the column STRUCT value. +For the previous array-select example, the following property can be mapped with the constructed ARRAY<STRUCT> column: List<TwoInts> pair; where the TwoInts type is defined: +class TwoInts { + + int val1; + + int val2; +} +
    +
    +Custom types +Custom converters can be used to extend the type support for user defined types. + + +Converters need to implement the org.springframework.core.convert.converter.Converter interface in both directions. + + +The user defined type needs to be mapped to one of the basic types supported by Spanner: + + +com.google.cloud.ByteArray + + +com.google.cloud.Date + + +com.google.cloud.Timestamp + + +java.lang.Boolean, boolean + + +java.lang.Double, double + + +java.lang.Long, long + + +java.lang.String + + +double[] + + +long[] + + +boolean[] + + +enum types + + + + +An instance of both Converters needs to be passed to a ConverterAwareMappingSpannerEntityProcessor, which then has to be made available as a @Bean for SpannerEntityProcessor. + + +For example: +We would like to have a field of type Person on our Trade POJO: +@Table(name = "trades") +public class Trade { + //... + Person person; + //... +} +Where Person is a simple class: +public class Person { + + public String firstName; + public String lastName; + +} +We have to define the two converters: + public class PersonWriteConverter implements Converter<Person, String> { + + @Override + public String convert(Person person) { + return person.firstName + " " + person.lastName; + } + } + + public class PersonReadConverter implements Converter<String, Person> { + + @Override + public Person convert(String s) { + Person person = new Person(); + person.firstName = s.split(" ")[0]; + person.lastName = s.split(" ")[1]; + return person; + } + } +That will be configured in our @Configuration file: +@Configuration +public class ConverterConfiguration { + + @Bean + public SpannerEntityProcessor spannerEntityProcessor(SpannerMappingContext spannerMappingContext) { + return new ConverterAwareMappingSpannerEntityProcessor(spannerMappingContext, + Arrays.asList(new PersonWriteConverter()), + Arrays.asList(new PersonReadConverter())); + } +} +
    +
    +Custom Converter for Struct Array Columns +If a Converter<Struct, A> is provided, then properties of type List<A> can be used in your entity types. +
    +
    +
    +Spanner Operations & Template +SpannerOperations and its implementation, SpannerTemplate, provides the Template pattern familiar to Spring developers. +It provides: + + +Resource management + + +One-stop-shop to Spanner operations with the Spring Data POJO mapping and conversion features + + +Exception conversion + + +Using the autoconfigure provided by our Spring Boot Starter for Spanner, your Spring application context will contain a fully configured SpannerTemplate object that you can easily autowire in your application: +@SpringBootApplication +public class SpannerTemplateExample { + + @Autowired + SpannerTemplate spannerTemplate; + + public void doSomething() { + this.spannerTemplate.delete(Trade.class, KeySet.all()); + //... + Trade t = new Trade(); + //... + this.spannerTemplate.insert(t); + //... + List<Trade> tradesByAction = spannerTemplate.findAll(Trade.class); + //... + } +} +The Template API provides convenience methods for: + + +Reads, and by providing SpannerReadOptions and +SpannerQueryOptions + + +Stale read + + +Read with secondary indices + + +Read with limits and offsets + + +Read with sorting + + + + +Queries + + +DML operations (delete, insert, update, upsert) + + +Partial reads + + +You can define a set of columns to be read into your entity + + + + +Partial writes + + +Persist only a few properties from your entity + + + + +Read-only transactions + + +Locking read-write transactions + + +
    +SQL Query +Cloud Spanner has SQL support for running read-only queries. +All the query related methods start with query on SpannerTemplate. +Using SpannerTemplate you can execute SQL queries that map to POJOs: +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades")); +
    +
    +Read +Spanner exposes a Read API for reading single row or multiple rows in a table or in a secondary index. +Using SpannerTemplate you can execute reads, for example: +List<Trade> trades = this.spannerTemplate.readAll(Trade.class); +Main benefit of reads over queries is reading multiple rows of a certain pattern of keys is much easier using the features of the KeySet class. +
    +
    +Advanced reads +
    +Stale read +All reads and queries are strong reads by default. +A strong read is a read at a current timestamp and is guaranteed to see all data that has been committed up until the start of this read. +A stale read on the other hand is read at a timestamp in the past. +Cloud Spanner allows you to determine how current the data should be when you read data. +With SpannerTemplate you can specify the Timestamp by setting it on SpannerQueryOptions or SpannerReadOptions to the appropriate read or query methods: +Reads: +// a read with options: +SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setTimestamp(Timestamp.now()); +List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions); +Queries: +// a query with options: +SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setTimestamp(Timestamp.now()); +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions); +
    +
    +Read from a secondary index +Using a secondary index is available for Reads via the Template API and it is also implicitly available via SQL for Queries. +The following shows how to read rows from a table using a secondary index simply by setting index on SpannerReadOptions: +SpannerReadOptions spannerReadOptions = new SpannerReadOptions().setIndex("TradesByTrader"); +List<Trade> trades = this.spannerTemplate.readAll(Trade.class, spannerReadOptions); +
    +
    +Read with offsets and limits +Limits and offsets are only supported by Queries. +The following will get only the first two rows of the query: +SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setLimit(2).setOffset(3); +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions); +Note that the above is equivalent of executing SELECT * FROM trades LIMIT 2 OFFSET 3. +
    +
    +Sorting +Reads by keys do not support sorting. +However, queries on the Template API support sorting through standard SQL and also via Spring Data Sort API: +List<Trade> trades = this.spannerTemplate.queryAll(Trade.class, Sort.by("action")); +If the provided sorted field name is that of a property of the domain type, then the column name corresponding to that property will be used in the query. +Otherwise, the given field name is assumed to be the name of the column in the Cloud Spanner table. +Sorting on columns of Cloud Spanner types STRING and BYTES can be done while ignoring case: +Sort.by(Order.desc("action").ignoreCase()) +
    +
    +Partial read +Partial read is only possible when using Queries. +In case the rows returned by the query have fewer columns than the entity that it will be mapped to, Spring Data will map the returned columns only. +This setting also applies to nested structs and their corresponding nested POJO properties. +List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT action, symbol FROM trades"), + new SpannerQueryOptions().setAllowMissingResultSetColumns(true)); +If the setting is set to false, then an exception will be thrown if there are missing columns in the query result. +
    +
    +Summary of options for Query vs Read + + + + + + + +Feature +Query supports it +Read supports it + + +SQL +yes +no + + +Partial read +yes +no + + +Limits +yes +no + + +Offsets +yes +no + + +Secondary index +yes +yes + + +Read using index range +no +yes + + +Sorting +yes +no + + + + +
    +
    +
    +Write / Update +The write methods of SpannerOperations accept a POJO and writes all of its properties to Spanner. +The corresponding Spanner table and entity metadata is obtained from the given object’s actual type. +If a POJO was retrieved from Spanner and its primary key properties values were changed and then written or updated, the operation will occur as if against a row with the new primary key values. +The row with the original primary key values will not be affected. +
    +Insert +The insert method of SpannerOperations accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if a row with the POJO’s primary key already exists in the table. +Trade t = new Trade(); +this.spannerTemplate.insert(t); +
    +
    +Update +The update method of SpannerOperations accepts a POJO and writes all of its properties to Spanner, which means the operation will fail if the POJO’s primary key does not already exist in the table. +// t was retrieved from a previous operation +this.spannerTemplate.update(t); +
    +
    +Upsert +The upsert method of SpannerOperations accepts a POJO and writes all of its properties to Spanner using update-or-insert. +// t was retrieved from a previous operation or it's new +this.spannerTemplate.upsert(t); +
    +
    +Partial Update +The update methods of SpannerOperations operate by default on all properties within the given object, but also accept String[] and Optional<Set<String>> of column names. +If the Optional of set of column names is empty, then all columns are written to Spanner. +However, if the Optional is occupied by an empty set, then no columns will be written. +// t was retrieved from a previous operation or it's new +this.spannerTemplate.update(t, "symbol", "action"); +
    +
    +
    +DML +DML statements can be executed using SpannerOperations.executeDmlStatement. +Inserts, updates, and deletions can affect any number of rows and entities. +
    +
    +Transactions +SpannerOperations provides methods to run java.util.Function objects within a single transaction while making available the read and write methods from SpannerOperations. +
    +Read/Write Transaction +Read and write transactions are provided by SpannerOperations via the performReadWriteTransaction method: +@Autowired +SpannerOperations mySpannerOperations; + +public String doWorkInsideTransaction() { + return mySpannerOperations.performReadWriteTransaction( + transActionSpannerOperations -> { + // Work with transActionSpannerOperations here. + // It is also a SpannerOperations object. + + return "transaction completed"; + } + ); +} +The performReadWriteTransaction method accepts a Function that is provided an instance of a SpannerOperations object. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular SpannerOperations with a few exceptions: + + +Its read functionality cannot perform stale reads, because all reads and writes happen at the single point in time of the transaction. + + +It cannot perform sub-transactions via performReadWriteTransaction or performReadOnlyTransaction. + + +As these read-write transactions are locking, it is recommended that you use the performReadOnlyTransaction if your function does not perform any writes. +
    +
    +Read-only Transaction +The performReadOnlyTransaction method is used to perform read-only transactions using a SpannerOperations: +@Autowired +SpannerOperations mySpannerOperations; + +public String doWorkInsideTransaction() { + return mySpannerOperations.performReadOnlyTransaction( + transActionSpannerOperations -> { + // Work with transActionSpannerOperations here. + // It is also a SpannerOperations object. + + return "transaction completed"; + } + ); +} +The performReadOnlyTransaction method accepts a Function that is provided an instance of a +SpannerOperations object. +This method also accepts a ReadOptions object, but the only attribute used is the timestamp used to determine the snapshot in time to perform the reads in the transaction. +If the timestamp is not set in the read options the transaction is run against the current state of the database. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular SpannerOperations with +a few exceptions: + + +Its read functionality cannot perform stale reads, because all reads happen at the single point in time of the transaction. + + +It cannot perform sub-transactions via performReadWriteTransaction or performReadOnlyTransaction + + +It cannot perform any write operations. + + +Because read-only transactions are non-locking and can be performed on points in time in the past, these are recommended for functions that do not perform write operations. +
    +
    +Declarative Transactions with @Transactional Annotation +This feature requires a bean of SpannerTransactionManager, which is provided when using spring-cloud-gcp-starter-data-spanner. +SpannerTemplate and SpannerRepository support running methods with the @Transactional [annotation](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative) as transactions. +If a method annotated with @Transactional calls another method also annotated, then both methods will work within the same transaction. +performReadOnlyTransaction and performReadWriteTransaction cannot be used in @Transactional annotated methods because Cloud Spanner does not support transactions within transactions. +
    +
    +
    +DML Statements +SpannerTemplate supports [DML](https://cloud.google.com/spanner/docs/dml-tasks) Statements. +DML statements can be executed in transactions via performReadWriteTransaction or using the @Transactional annotation. +When DML statements are executed outside of transactions, they are executed in [partitioned-mode](https://cloud.google.com/spanner/docs/dml-tasks#partitioned-dml). +
    +
    +
    +Repositories +Spring Data Repositories are a powerful abstraction that can save you a lot of boilerplate code. +For example: +public interface TraderRepository extends SpannerRepository<Trader, String> { +} +Spring Data generates a working implementation of the specified interface, which can be conveniently autowired into an application. +The Trader type parameter to SpannerRepository refers to the underlying domain type. +The second type parameter, String in this case, refers to the type of the key of the domain type. +For POJOs with a composite primary key, this ID type parameter can be any descendant of Object[] compatible with all primary key properties, any descendant of Iterable, or com.google.cloud.spanner.Key. +If the domain POJO type only has a single primary key column, then the primary key property type can be used or the Key type. +For example in case of Trades, that belong to a Trader, TradeRepository would look like this: +public interface TradeRepository extends SpannerRepository<Trade, String[]> { + +} +public class MyApplication { + + @Autowired + SpannerTemplate spannerTemplate; + + @Autowired + StudentRepository studentRepository; + + public void demo() { + + this.tradeRepository.deleteAll(); + String traderId = "demo_trader"; + Trade t = new Trade(); + t.symbol = stock; + t.action = action; + t.traderId = traderId; + t.price = 100.0; + t.shares = 12345.6; + this.spannerTemplate.insert(t); + + Iterable<Trade> allTrades = this.tradeRepository.findAll(); + + int count = this.tradeRepository.countByAction("BUY"); + + } +} +
    +CRUD Repository +CrudRepository methods work as expected, with one thing Spanner specific: the save and saveAll methods work as update-or-insert. +
    +
    +Paging and Sorting Repository +You can also use PagingAndSortingRepository with Spanner Spring Data. +The sorting and pageable findAll methods available from this interface operate on the current state of the Spanner database. +As a result, beware that the state of the database (and the results) might change when moving page to page. +
    +
    +Spanner Repository +The SpannerRepository extends the PagingAndSortingRepository, but adds the read-only and the read-write transaction functionality provided by Spanner. +These transactions work very similarly to those of SpannerOperations, but is specific to the repository’s domain type and provides repository functions instead of template functions. +For example, this is a read-write transaction: +@Autowired +SpannerRepository myRepo; + +public String doWorkInsideTransaction() { + return myRepo.performReadOnlyTransaction( + transactionSpannerRepo -> { + // Work with the single-transaction transactionSpannerRepo here. + // This is a SpannerRepository object. + + return "transaction completed"; + } + ); +} +When creating custom repositories for your own domain types and query methods, you can extend SpannerRepository to access Cloud Spanner-specific features as well as all features from PagingAndSortingRepository and CrudRepository. +
    +
    +
    +Query Methods +SpannerRepository supports Query Methods. +Described in the following sections, these are methods residing in your custom repository interfaces of which implementations are generated based on their names and annotations. +Query Methods can read, write, and delete entities in Cloud Spanner. +Parameters to these methods can be any Cloud Spanner data type supported directly or via custom configured converters. +Parameters can also be of type Struct or POJOs. +If a POJO is given as a parameter, it will be converted to a Struct with the same type-conversion logic as used to create write mutations. +Comparisons using Struct parameters are limited to what is available with Cloud Spanner. +
    +Query methods by convention +public interface TradeRepository extends SpannerRepository<Trade, String[]> { + List<Trade> findByAction(String action); + + int countByAction(String action); + + // Named methods are powerful, but can get unwieldy + List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc( + String action, String symbol, String traderId); +} +In the example above, the query methods in TradeRepository are generated based on the name of the methods, using the Spring Data Query creation naming convention. +List<Trade> findByAction(String action) would translate to a SELECT * FROM trades WHERE action = ?. +The function List<Trade> findTop3DistinctByActionAndSymbolIgnoreCaseOrTraderIdOrderBySymbolDesc(String action, String symbol, String traderId); will be translated as the equivalent of this SQL query: +SELECT DISTINCT * FROM trades +WHERE ACTION = ? AND LOWER(SYMBOL) = LOWER(?) AND TRADER_ID = ? +ORDER BY SYMBOL DESC +LIMIT 3 +The following filter options are supported: + + +Equality + + +Greater than or equals + + +Greater than + + +Less than or equals + + +Less than + + +Is null + + +Is not null + + +Is true + + +Is false + + +Like a string + + +Not like a string + + +Contains a string + + +Not contains a string + + +Note that the phrase SymbolIgnoreCase is translated to LOWER(SYMBOL) = LOWER(?) indicating a non-case-sensitive matching. +The IgnoreCase phrase may only be appended to fields that correspond to columns of type STRING or BYTES. +The Spring Data "AllIgnoreCase" phrase appended at the end of the method name is not supported. +The Like or NotLike naming conventions: +List<Trade> findBySymbolLike(String symbolFragment); +The param symbolFragment can contain wildcard characters for string matching such as _ and %. +The Contains and NotContains naming conventions: +List<Trade> findBySymbolContains(String symbolFragment); +The param symbolFragment is a regular expression that is checked for occurrences. +Delete queries are also supported. +For example, query methods such as deleteByAction or removeByAction delete entities found by findByAction. +The delete operation happens in a single transaction. +Delete queries can have the following return types: +* An integer type that is the number of entities deleted +* A collection of entities that were deleted +* void +
    +
    +Custom SQL/DML query methods +The example above for List<Trade> fetchByActionNamedQuery(String action) does not match the Spring Data Query creation naming convention, so we have to map a parametrized Spanner SQL query to it. +The SQL query for the method can be mapped to repository methods in one of two ways: + + +namedQueries properties file + + +using the @Query annotation + + +The names of the tags of the SQL correspond to the @Param annotated names of the method parameters. +Custom SQL query methods can accept a single Sort or Pageable parameter that is applied on top of any sorting or paging in the SQL: + @Query("SELECT * FROM trades ORDER BY action DESC") + List<Trade> sortedTrades(Pageable pageable); + + @Query("SELECT * FROM trades ORDER BY action DESC LIMIT 1") + Trade sortedTopTrade(Pageable pageable); +This can be used: + List<Trade> customSortedTrades = tradeRepository.sortedTrades(PageRequest + .of(2, 2, org.springframework.data.domain.Sort.by(Order.asc("id")))); +The results would be sorted by "id" in ascending order. +Your query method can also return non-entity types: + @Query("SELECT COUNT(1) FROM trades WHERE action = @action") + int countByActionQuery(String action); + + @Query("SELECT EXISTS(SELECT COUNT(1) FROM trades WHERE action = @action)") + boolean existsByActionQuery(String action); + + @Query("SELECT action FROM trades WHERE action = @action LIMIT 1") + String getFirstString(@Param("action") String action); + + @Query("SELECT action FROM trades WHERE action = @action") + List<String> getFirstStringList(@Param("action") String action); +DML statements can also be executed by query methods, but the only possible return value is a long representing the number of affected rows. +The dmlStatement boolean setting must be set on @Query to indicate that the query method is executed as a DML statement. + @Query(value = "DELETE FROM trades WHERE action = @action", dmlStatement = true) + long deleteByActionQuery(String action); +
    +Query methods with named queries properties +By default, the namedQueriesLocation attribute on @EnableSpannerRepositories points to the META-INF/spanner-named-queries.properties file. +You can specify the query for a method in the properties file by providing the SQL as the value for the "interface.method" property: +Trade.fetchByActionNamedQuery=SELECT * FROM trades WHERE trades.action = @tag0 +public interface TradeRepository extends SpannerRepository<Trade, String[]> { + // This method uses the query from the properties file instead of one generated based on name. + List<Trade> fetchByActionNamedQuery(@Param("tag0") String action); +} +
    +
    +Query methods with annotation +Using the @Query annotation: +public interface TradeRepository extends SpannerRepository<Trade, String[]> { + @Query("SELECT * FROM trades WHERE trades.action = @tag0") + List<Trade> fetchByActionNamedQuery(@Param("tag0") String action); +} +Table names can be used directly. +For example, "trades" in the above example. +Alternatively, table names can be resolved from the @Table annotation on domain classes as well. +In this case, the query should refer to table names with fully qualified class names between : +characters: :fully.qualified.ClassName:. +A full example would look like: +@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0") +List<Trade> fetchByActionNamedQuery(String action); +This allows table names evaluated with SpEL to be used in custom queries. +SpEL can also be used to provide SQL parameters: +@Query("SELECT * FROM :com.example.Trade: WHERE trades.action = @tag0 + AND price > #{#priceRadius * -1} AND price < #{#priceRadius * 2}") +List<Trade> fetchByActionNamedQuery(String action, Double priceRadius); +
    +
    +
    +Projections +Spring Data Spanner supports projections. +You can define projection interfaces based on domain types and add query methods that return them in your repository: +public interface TradeProjection { + + String getAction(); + + @Value("#{target.symbol + ' ' + target.action}") + String getSymbolAndAction(); +} + +public interface TradeRepository extends SpannerRepository<Trade, Key> { + + List<Trade> findByTraderId(String traderId); + + List<TradeProjection> findByAction(String action); + + @Query("SELECT action, symbol FROM trades WHERE action = @action") + List<TradeProjection> findByQuery(String action); +} +Projections can be provided by name-convention-based query methods as well as by custom SQL queries. +If using custom SQL queries, you can further restrict the columns retrieved from Spanner to just those required by the projection to improve performance. +Properties of projection types defined using SpEL use the fixed name target for the underlying domain object. +As a result accessing underlying properties take the form target.<property-name>. +
    +
    +REST Repositories +When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-rest</artifactId> +</dependency> +If you prefer to configure parameters (such as path), you can use @RepositoryRestResource annotation: +@RepositoryRestResource(collectionResourceRel = "trades", path = "trades") +public interface TradeRepository extends SpannerRepository<Trade, String[]> { +} +For example, you can retrieve all Trade objects in the repository by using curl http://<server>:<port>/trades, or any specific trade via curl http://<server>:<port>/trades/<trader_id>,<trade_id>. +The separator between your primary key components, id and trader_id in this case, is a comma by default, but can be configured to any string not found in your key values by extending the SpannerKeyIdConverter class: +@Component +class MySpecialIdConverter extends SpannerKeyIdConverter { + + @Override + protected String getUrlIdSeparator() { + return ":"; + } +} +You can also write trades using curl -XPOST -H"Content-Type: application/json" -d@test.json http://<server>:<port>/trades/ where the file test.json holds the JSON representation of a Trade object. +
    +
    +
    +Database and Schema Admin +Databases and tables inside Spanner instances can be created automatically from SpannerPersistentEntity objects: +@Autowired +private SpannerSchemaUtils spannerSchemaUtils; + +@Autowired +private SpannerDatabaseAdminTemplate spannerDatabaseAdminTemplate; + +public void createTable(SpannerPersistentEntity entity) { + if(!spannerDatabaseAdminTemplate.tableExists(entity.tableName()){ + + // The boolean parameter indicates that the database will be created if it does not exist. + spannerDatabaseAdminTemplate.executeDdlStrings(Arrays.asList( + spannerSchemaUtils.getCreateTableDDLString(entity.getType())), true); + } +} +Schemas can be generated for entire object hierarchies with interleaved relationships and composite keys. +
    +
    +Sample +A sample application is available. +
    +
    + +Spring Data Cloud Datastore +Spring Data is an abstraction for storing and retrieving POJOs in numerous storage technologies. +Spring Cloud GCP adds Spring Data support for Google Cloud Datastore. +Maven coordinates for this module only, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-data-datastore</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-datastore' +} +We provide a Spring Boot Starter for Spring Data Datastore, with which you can use our recommended auto-configuration setup. +To use the starter, see the coordinates below. +Maven: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-data-datastore</artifactId> +</dependency> +Gradle: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-datastore' +} +This setup takes care of bringing in the latest compatible version of Cloud Java Cloud Datastore libraries as well. +
    +Configuration +To setup Spring Data Cloud Datastore, you have to configure the following: + + +Setup the connection details to Google Cloud Datastore. + + +
    +Cloud Datastore settings +You can the use Spring Boot Starter for Spring Data Datastore to autoconfigure Google Cloud Datastore in your Spring application. +It contains all the necessary setup that makes it easy to authenticate with your Google Cloud project. +The following configuration options are available: + + + + + + + + +Name +Description +Required +Default value + + +spring.cloud.gcp.datastore.enabled +Enables the Cloud Datastore client +No +true + + +spring.cloud.gcp.datastore.project-id +GCP project ID where the Google Cloud Datastore API is hosted, if different from the one in the Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.datastore.credentials.location +OAuth2 credentials for authenticating with the +Google Cloud Datastore API, if different from the ones in the +Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.datastore.credentials.encoded-key +Base64-encoded OAuth2 credentials for authenticating with the +Google Cloud Datastore API, if different from the ones in the +Spring Cloud GCP Core Module +No + + + +spring.cloud.gcp.datastore.credentials.scopes +OAuth2 scope for Spring Cloud GCP +Cloud Datastore credentials +No +https://www.googleapis.com/auth/datastore + + +spring.cloud.gcp.datastore.namespace +The Cloud Datastore namespace to use +No +the Default namespace of Cloud Datastore in your GCP project + + + + +
    +
    +Repository settings +Spring Data Repositories can be configured via the @EnableDatastoreRepositories annotation on your main @Configuration class. +With our Spring Boot Starter for Spring Data Cloud Datastore, @EnableDatastoreRepositories is automatically added. +It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by @EnableDatastoreRepositories. +
    +
    +Autoconfiguration +Our Spring Boot autoconfiguration creates the following beans available in the Spring application context: + + +an instance of DatastoreTemplate + + +an instance of all user defined repositories extending CrudRepository, PagingAndSortingRepository, and DatastoreRepository (an extension of PagingAndSortingRepository with additional Cloud Datastore features) when repositories are enabled + + +an instance of Datastore from the Google Cloud Java Client for Datastore, for convenience and lower level API access + + +
    +
    +
    +Object Mapping +Spring Data Cloud Datastore allows you to map domain POJOs to Cloud Datastore kinds and entities via annotations: +@Entity(name = "traders") +public class Trader { + + @Id + @Field(name = "trader_id") + String traderId; + + String firstName; + + String lastName; + + @Transient + Double temporaryNumber; +} +Spring Data Cloud Datastore will ignore any property annotated with @Transient. +These properties will not be written to or read from Cloud Datastore. +
    +Constructors +Simple constructors are supported on POJOs. +The constructor arguments can be a subset of the persistent properties. +Every constructor argument needs to have the same name and type as a persistent property on the entity and the constructor should set the property from the given argument. +Arguments that are not directly set to properties are not supported. +@Entity(name = "traders") +public class Trader { + + @Id + @Field(name = "trader_id") + String traderId; + + String firstName; + + String lastName; + + @Transient + Double temporaryNumber; + + public Trader(String traderId, String firstName) { + this.traderId = traderId; + this.firstName = firstName; + } +} +
    +
    +Kind +The @Entity annotation can provide the name of the Cloud Datastore kind that stores instances of the annotated class, one per row. +
    +
    +Keys +@Id identifies the property corresponding to the ID value. +You must annotate one of your POJO’s fields as the ID value, because every entity in Cloud Datastore requires a single ID value: +@Entity(name = "trades") +public class Trade { + @Id + @Field(name = "trade_id") + String tradeId; + + @Field(name = "trader_id") + String traderId; + + String action; + + Double price; + + Double shares; + + String symbol; +} +Datastore can automatically allocate integer ID values. +If a POJO instance with a Long ID property is written to Cloud Datastore with null as the ID value, then Spring Data Cloud Datastore will obtain a newly allocated ID value from Cloud Datastore and set that in the POJO for saving. +Because primitive long ID properties cannot be null and default to 0, keys will not be allocated. +
    +
    +Fields +All accessible properties on POJOs are automatically recognized as a Cloud Datastore field. +Field naming is generated by the PropertyNameFieldNamingStrategy by default defined on the DatastoreMappingContext bean. +The @Field annotation optionally provides a different field name than that of the property. +
    +
    +Supported Types +Spring Data Cloud Datastore supports the following types for regular fields and elements of collections: + + + + + + +Type +Stored as + + + + +com.google.cloud.Timestamp +com.google.cloud.datastore.TimestampValue + + +com.google.cloud.datastore.Blob +com.google.cloud.datastore.BlobValue + + +com.google.cloud.datastore.LatLng +com.google.cloud.datastore.LatLngValue + + +java.lang.Boolean, boolean +com.google.cloud.datastore.BooleanValue + + +java.lang.Double, double +com.google.cloud.datastore.DoubleValue + + +java.lang.Long, long +com.google.cloud.datastore.LongValue + + +java.lang.Integer, int +com.google.cloud.datastore.LongValue + + +java.lang.String +com.google.cloud.datastore.StringValue + + +com.google.cloud.datastore.Entity +com.google.cloud.datastore.EntityValue + + +com.google.cloud.datastore.Key +com.google.cloud.datastore.KeyValue + + +byte[] +com.google.cloud.datastore.BlobValue + + +Java enum values +com.google.cloud.datastore.StringValue + + + + +In addition, all types that can be converted to the ones listed in the table by +org.springframework.core.convert.support.DefaultConversionService are supported. +
    +
    +Custom types +Custom converters can be used extending the type support for user defined types. + + +Converters need to implement the org.springframework.core.convert.converter.Converter interface in both directions. + + +The user defined type needs to be mapped to one of the basic types supported by Cloud Datastore. + + +An instance of both Converters (read and write) needs to be passed to the DatastoreCustomConversions constructor, which then has to be made available as a @Bean for DatastoreCustomConversions. + + +For example: +We would like to have a field of type Album on our Singer POJO and want it to be stored as a string property: +@Entity +public class Singer { + + @Id + String singerId; + + String name; + + Album album; +} +Where Album is a simple class: +public class Album { + String albumName; + + LocalDate date; +} +We have to define the two converters: + //Converter to write custom Album type + static final Converter<Album, String> ALBUM_STRING_CONVERTER = + new Converter<Album, String>() { + @Override + public String convert(Album album) { + return album.getAlbumName() + " " + album.getDate().format(DateTimeFormatter.ISO_DATE); + } + }; + + //Converters to read custom Album type + static final Converter<String, Album> STRING_ALBUM_CONVERTER = + new Converter<String, Album>() { + @Override + public Album convert(String s) { + String[] parts = s.split(" "); + return new Album(parts[0], LocalDate.parse(parts[parts.length - 1], DateTimeFormatter.ISO_DATE)); + } + }; +That will be configured in our @Configuration file: +@Configuration +public class ConverterConfiguration { + @Bean + public DatastoreCustomConversions datastoreCustomConversions() { + return new DatastoreCustomConversions( + Arrays.asList( + ALBUM_STRING_CONVERTER, + STRING_ALBUM_CONVERTER)); + } +} +
    +
    +Collections and arrays +Arrays and collections (types that implement java.util.Collection) of supported types are supported. +They are stored as com.google.cloud.datastore.ListValue. +Elements are converted to Cloud Datastore supported types individually. byte[] is an exception, it is converted to +com.google.cloud.datastore.Blob. +
    +
    +Custom Converter for collections +Users can provide converters from List<?> to the custom collection type. +Only read converter is necessary, the Collection API is used on the write side to convert a collection to the internal list type. +Collection converters need to implement the org.springframework.core.convert.converter.Converter interface. +Example: +Let’s improve the Singer class from the previous example. +Instead of a field of type Album, we would like to have a field of type ImmutableSet<Album>: +@Entity +public class Singer { + + @Id + String singerId; + + String name; + + ImmutableSet<Album> albums; +} +We have to define a read converter only: +static final Converter<List<?>, ImmutableSet<?>> LIST_IMMUTABLE_SET_CONVERTER = + new Converter<List<?>, ImmutableSet<?>>() { + @Override + public ImmutableSet<?> convert(List<?> source) { + return ImmutableSet.copyOf(source); + } + }; +And add it to the list of custom converters: +@Configuration +public class ConverterConfiguration { + @Bean + public DatastoreCustomConversions datastoreCustomConversions() { + return new DatastoreCustomConversions( + Arrays.asList( + LIST_IMMUTABLE_SET_CONVERTER, + + ALBUM_STRING_CONVERTER, + STRING_ALBUM_CONVERTER)); + } +} +
    +
    +
    +Relationships +There are three ways to represent relationships between entities that are described in this section: + + +Embedded entities stored directly in the field of the containing entity + + +@Descendant annotated properties for one-to-many relationships + + +@Reference annotated properties for general relationships without hierarchy + + +
    +Embedded Entities +Fields whose types are also annotated with @Entity are converted to EntityValue and stored inside the parent entity. +Here is an example of Cloud Datastore entity containing an embedded entity in JSON: +{ + "name" : "Alexander", + "age" : 47, + "child" : {"name" : "Philip" } +} +This corresponds to a simple pair of Java entities: +import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity; +import org.springframework.data.annotation.Id; + +@Entity("parents") +public class Parent { + @Id + String name; + + Child child; +} + +@Entity +public class Child { + String name; +} +Child entities are not stored in their own kind. +They are stored in their entirety in the child field of the parents kind. +Multiple levels of embedded entities are supported. + +Embedded entities don’t need to have @Id field, it is only required for top level entities. + +Example: +Entities can hold embedded entities that are their own type. +We can store trees in Cloud Datastore using this feature: +import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded; +import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity; +import org.springframework.data.annotation.Id; + +@Entity +public class EmbeddableTreeNode { + @Id + long value; + + EmbeddableTreeNode left; + + EmbeddableTreeNode right; + + Map<String, Long> longValues; + + Map<String, List<Timestamp>> listTimestamps; + + public EmbeddableTreeNode(long value, EmbeddableTreeNode left, EmbeddableTreeNode right) { + this.value = value; + this.left = left; + this.right = right; + } +} +
    +Maps +Maps will be stored as embedded entities where the key values become the field names in the embedded entity. +The value types in these maps can be any regularly supported property type, and the key values will be converted to String using the configured converters. +Also, a collection of entities can be embedded; it will be converted to ListValue on write. +Example: +Instead of a binary tree from the previous example, we would like to store a general tree +(each node can have an arbitrary number of children) in Cloud Datastore. +To do that, we need to create a field of type List<EmbeddableTreeNode>: +import org.springframework.cloud.gcp.data.datastore.core.mapping.Embedded; +import org.springframework.data.annotation.Id; + +public class EmbeddableTreeNode { + @Id + long value; + + List<EmbeddableTreeNode> children; + + Map<String, EmbeddableTreeNode> siblingNodes; + + Map<String, Set<EmbeddableTreeNode>> subNodeGroups; + + public EmbeddableTreeNode(List<EmbeddableTreeNode> children) { + this.children = children; + } +} +Because Maps are stored as entities, they can further hold embedded entities: + + +Singular embedded objects in the value can be stored in the values of embedded Maps. + + +Collections of embedded objects in the value can also be stored as the values of embedded Maps. + + +Maps in the value are further stored as embedded entities with the same rules applied recursively for their values. + + +
    +
    +
    +Ancestor-Descendant Relationships +Parent-child relationships are supported via the @Descendants annotation. +Unlike embedded children, descendants are fully-formed entities residing in their own kinds. +The parent entity does not have an extra field to hold the descendant entities. +Instead, the relationship is captured in the descendants' keys, which refer to their parent entities: +import org.springframework.cloud.gcp.data.datastore.core.mapping.Descendants; +import org.springframework.cloud.gcp.data.datastore.core.mapping.Entity; +import org.springframework.data.annotation.Id; + +@Entity("orders") +public class ShoppingOrder { + @Id + long id; + + @Descendants + List<Item> items; +} + +@Entity("purchased_item") +public class Item { + @Id + Key purchasedItemKey; + + String name; + + Timestamp timeAddedToOrder; +} +For example, an instance of a GQL key-literal representation for Item would also contain the parent ShoppingOrder ID value: +Key(orders, '12345', purchased_item, 'eggs') +The GQL key-literal representation for the parent ShoppingOrder would be: +Key(orders, '12345') +The Cloud Datastore entities exist separately in their own kinds. +The ShoppingOrder: +{ + "id" : 12345 +} +The two items inside that order: +{ + "purchasedItemKey" : Key(orders, '12345', purchased_item, 'eggs'), + "name" : "eggs", + "timeAddedToOrder" : "2014-09-27 12:30:00.45-8:00" +} + +{ + "purchasedItemKey" : Key(orders, '12345', purchased_item, 'sausage'), + "name" : "sausage", + "timeAddedToOrder" : "2014-09-28 11:30:00.45-9:00" +} +The parent-child relationship structure of objects is stored in Cloud Datastore using Datastore’s ancestor relationships. +Because the relationships are defined by the Ancestor mechanism, there is no extra column needed in either the parent or child entity to store this relationship. +The relationship link is part of the descendant entity’s key value. +These relationships can be many levels deep. +Properties holding child entities must be collection-like, but they can be any of the supported inter-convertible collection-like types that are supported for regular properties such as List, arrays, Set, etc…​ +Child items must have Key as their ID type because Cloud Datastore stores the ancestor relationship link inside the keys of the children. +Reading or saving an entity automatically causes all subsequent levels of children under that entity to be read or saved, respectively. +If a new child is created and added to a property annotated @Descendants and the key property is left null, then a new key will be allocated for that child. +The ordering of the retrieved children may not be the same as the ordering in the original property that was saved. +Child entities cannot be moved from the property of one parent to that of another unless the child’s key property is set to null or a value that contains the new parent as an ancestor. +Since Cloud Datastore entity keys can have multiple parents, it is possible that a child entity appears in the property of multiple parent entities. +Because entity keys are immutable in Cloud Datastore, to change the key of a child you must delete the existing one and re-save it with the new key. +
    +
    +Key Reference Relationships +General relationships can be stored using the @Reference annotation. +import org.springframework.cloud.gcp.data.datastore.core.mapping.Reference; +import org.springframework.data.annotation.Id; + +@Entity +public class ShoppingOrder { + @Id + long id; + + @Reference + List<Item> items; + + @Reference + Item specialSingleItem; +} + +@Entity +public class Item { + @Id + Key purchasedItemKey; + + String name; + + Timestamp timeAddedToOrder; +} +@Reference relationships are between fully-formed entities residing in their own kinds. +The relationship between ShoppingOrder and Item entities are stored as a Key field inside ShoppingOrder, which are resolved to the underlying Java entity type by Spring Data Cloud Datastore: +{ + "id" : 12345, + "specialSingleItem" : Key(item, "milk"), + "items" : [ Key(item, "eggs"), Key(item, "sausage") ] +} +Reference properties can either be singular or collection-like. +These properties correspond to actual columns in the entity and Cloud Datastore Kind that hold the key values of the referenced entities. +The referenced entities are full-fledged entities of other Kinds. +Similar to the @Descendants relationships, reading or writing an entity will recursively read or write all of the referenced entities at all levels. +If referenced entities have null ID values, then they will be saved as new entities and will have ID values allocated by Cloud Datastore. +There are no requirements for relationships between the key of an entity and the keys that entity holds as references. +The order of collection-like reference properties is not preserved when reading back from Cloud Datastore. +
    +
    +
    +Datastore Operations & Template +DatastoreOperations and its implementation, DatastoreTemplate, provides the Template pattern familiar to Spring developers. +Using the auto-configuration provided by Spring Boot Starter for Datastore, your Spring application context will contain a fully configured DatastoreTemplate object that you can autowire in your application: +@SpringBootApplication +public class DatastoreTemplateExample { + + @Autowired + DatastoreTemplate datastoreTemplate; + + public void doSomething() { + this.datastoreTemplate.deleteAll(Trader.class); + //... + Trader t = new Trader(); + //... + this.datastoreTemplate.save(t); + //... + List<Trader> traders = datastoreTemplate.findAll(Trader.class); + //... + } +} +The Template API provides convenience methods for: + + +Write operations (saving and deleting) + + +Read-write transactions + + +
    +GQL Query +In addition to retrieving entities by their IDs, you can also submit queries. + <T> Iterable<T> query(Query<? extends BaseEntity> query, Class<T> entityClass); + + <A, T> Iterable<T> query(Query<A> query, Function<A, T> entityFunc); + + Iterable<Key> queryKeys(Query<Key> query); +These methods, respectively, allow querying for: +* entities mapped by a given entity class using all the same mapping and converting features +* arbitrary types produced by a given mapping function +* only the Cloud Datastore keys of the entities found by the query +
    +
    +Find by ID(s) +Datstore reading a single entity or multiple entities in a kind. +Using DatastoreTemplate you can execute reads, for example: +Trader trader = this.datastoreTemplate.findById("trader1", Trader.class); + +List<Trader> traders = this.datastoreTemplate.findAllById(ImmutableList.of("trader1", "trader2"), Trader.class); + +List<Trader> allTraders = this.datastoreTemplate.findAll(Trader.class); +Cloud Datastore executes key-based reads with strong consistency, but queries with eventual consistency. +In the example above the first two reads utilize keys, while the third is executed using a query based on the corresponding Kind of Trader. +
    +Indexes +By default, all fields are indexed. +To disable indexing on a particular field, @Unindexed annotation can be used. +Example: +import org.springframework.cloud.gcp.data.datastore.core.mapping.Unindexed; + +public class ExampleItem { + long indexedField; + + @Unindexed + long unindexedField; +} +When using queries directly or via Query Methods, Cloud Datastore requires composite custom indexes if the select statement is not SELECT * or if there is more than one filtering condition in the WHERE clause. +
    +
    +Read with offsets, limits, and sorting +DatastoreRepository and custom-defined entity repositories implement the Spring Data PagingAndSortingRepository, which supports offsets and limits using page numbers and page sizes. +Paging and sorting options are also supported in DatastoreTemplate by supplying a DatastoreQueryOptions to findAll. +
    +
    +Partial read +This feature is not supported yet. +
    +
    +
    +Write / Update +The write methods of DatastoreOperations accept a POJO and writes all of its properties to Datastore. +The required Datastore kind and entity metadata is obtained from the given object’s actual type. +If a POJO was retrieved from Datastore and its ID value was changed and then written or updated, the operation will occur as if against a row with the new ID value. +The entity with the original ID value will not be affected. +Trader t = new Trader(); +this.datastoreTemplate.save(t); +The save method behaves as update-or-insert. +
    +Partial Update +This feature is not supported yet. +
    +
    +
    +Transactions +Read and write transactions are provided by DatastoreOperations via the performTransaction method: +@Autowired +DatastoreOperations myDatastoreOperations; + +public String doWorkInsideTransaction() { + return myDatastoreOperations.performTransaction( + transactionDatastoreOperations -> { + // Work with transactionDatastoreOperations here. + // It is also a DatastoreOperations object. + + return "transaction completed"; + } + ); +} +The performTransaction method accepts a Function that is provided an instance of a DatastoreOperations object. +The final returned value and type of the function is determined by the user. +You can use this object just as you would a regular DatastoreOperations with an exception: + + +It cannot perform sub-transactions. + + +Because of Cloud Datastore’s consistency guarantees, there are limitations to the operations and relationships among entities used inside transactions. +
    +Declarative Transactions with @Transactional Annotation +This feature requires a bean of DatastoreTransactionManager, which is provided when using spring-cloud-gcp-starter-data-datastore. +DatastoreTemplate and DatastoreRepository support running methods with the @Transactional annotation as transactions. +If a method annotated with @Transactional calls another method also annotated, then both methods will work within the same transaction. +performTransaction cannot be used in @Transactional annotated methods because Cloud Datastore does not support transactions within transactions. +
    +
    +
    +Read-Write Support for Maps +You can work with Maps of type Map<String, ?> instead of with entity objects by directly reading and writing them to and from Cloud Datastore. + +This is a different situation than using entity objects that contain Map properties. + +The map keys are used as field names for a Datastore entity and map values are converted to Datastore supported types. +Only simple types are supported (i.e. collections are not supported). +Converters for custom value types can be added (see section). +Example: +Map<String, Long> map = new HashMap<>(); +map.put("field1", 1L); +map.put("field2", 2L); +map.put("field3", 3L); + +keyForMap = datastoreTemplate.createKey("kindName", "id"); + +//write a map +datastoreTemplate.writeMap(keyForMap, map); + +//read a map +Map<String, Long> loadedMap = datastoreTemplate.findByIdAsMap(keyForMap, Long.class); +
    +
    +
    +Repositories +Spring Data Repositories are an abstraction that can reduce boilerplate code. +For example: +public interface TraderRepository extends DatastoreRepository<Trader, String> { +} +Spring Data generates a working implementation of the specified interface, which can be autowired into an application. +The Trader type parameter to DatastoreRepository refers to the underlying domain type. +The second type parameter, String in this case, refers to the type of the key of the domain type. +public class MyApplication { + + @Autowired + TraderRepository traderRepository; + + public void demo() { + + this.traderRepository.deleteAll(); + String traderId = "demo_trader"; + Trader t = new Trader(); + t.traderId = traderId; + this.tradeRepository.save(t); + + Iterable<Trader> allTraders = this.traderRepository.findAll(); + + int count = this.traderRepository.count(); + } +} +Repositories allow you to define custom Query Methods (detailed in the following sections) for retrieving, counting, and deleting based on filtering and paging parameters. +Filtering parameters can be of types supported by your configured custom converters. +
    +Query methods by convention +public interface TradeRepository extends DatastoreRepository<Trade, String[]> { + List<Trader> findByAction(String action); + + int countByAction(String action); + + boolean existsByAction(String action); + + List<Trade> findTop3ByActionAndSymbolAndPriceGreaterThanAndPriceLessThanOrEqualOrderBySymbolDesc( + String action, String symbol, double priceFloor, double priceCeiling); + + Page<TestEntity> findByAction(String action, Pageable pageable); + + Slice<TestEntity> findBySymbol(String symbol, Pageable pageable); + + List<TestEntity> findBySymbol(String symbol, Sort sort); +} +In the example above the query methods in TradeRepository are generated based on the name of the methods using thehttps://docs.spring.io/spring-data/data-commons/docs/current/reference/html#repositories.query-methods.query-creation[Spring Data Query creation naming convention]. +Cloud Datastore only supports filter components joined by AND, and the following operations: + + +equals + + +greater than or equals + + +greater than + + +less than or equals + + +less than + + +is null + + +After writing a custom repository interface specifying just the signatures of these methods, implementations are generated for you and can be used with an auto-wired instance of the repository. +Because of Cloud Datastore’s requirement that explicitly selected fields must all appear in a composite index together, find name-based query methods are run as SELECT *. +Delete queries are also supported. +For example, query methods such as deleteByAction or removeByAction delete entities found by findByAction. +Delete queries are executed as separate read and delete operations instead of as a single transaction because Cloud Datastore cannot query in transactions unless ancestors for queries are specified. +As a result, removeBy and deleteBy name-convention query methods cannot be used inside transactions via either performInTransaction or @Transactional annotation. +Delete queries can have the following return types: + + +An integer type that is the number of entities deleted + + +A collection of entities that were deleted + + +'void' + + +Methods can have org.springframework.data.domain.Pageable parameter to control pagination and sorting, or org.springframework.data.domain.Sort parameter to control sorting only. +See Spring Data documentation for details. +For returning multiple items in a repository method, we support Java collections as well as org.springframework.data.domain.Page and org.springframework.data.domain.Slice. +If a method’s return type is org.springframework.data.domain.Page, the returned object will include current page, total number of results and total number of pages. + +Methods that return Page execute an additional query to compute total number of pages. +Methods that return Slice, on the other hand, don’t execute any additional queries and therefore are much more efficient. + +
    +
    +Custom GQL query methods +Custom GQL queries can be mapped to repository methods in one of two ways: + + +namedQueries properties file + + +using the @Query annotation + + +
    +Query methods with annotation +Using the @Query annotation: +The names of the tags of the GQL correspond to the @Param annotated names of the method parameters. +public interface TraderRepository extends DatastoreRepository<Trader, String> { + + @Query("SELECT * FROM traders WHERE name = @trader_name") + List<Trader> tradersByName(@Param("trader_name") String traderName); + + @Query("SELECT * FROM test_entities_ci WHERE id = @id_val") + TestEntity getOneTestEntity(@Param("id_val") long id); +} +The following parameter types are supported: + + +com.google.cloud.Timestamp + + +com.google.cloud.datastore.Blob + + +com.google.cloud.datastore.Key + + +com.google.cloud.datastore.Cursor + + +java.lang.Boolean + + +java.lang.Double + + +java.lang.Long + + +java.lang.String + + +enum values. +These are queried as String values. + + +With the exception of Cursor, array forms of each of the types are also supported. +If you would like to obtain the count of items of a query or if there are any items returned by the query, set the count = true or exists = true properties of the @Query annotation, respectively. +The return type of the query method in these cases should be an integer type or a boolean type. +Cloud Datastore provides provides the SELECT key FROM …​ special column for all kinds that retrieves the Key`s of each row. +Selecting this special `key column is especially useful and efficient for count and exists queries. +You can also query for non-entity types: + @Query(value = "SELECT __key__ from test_entities_ci") + List<Key> getKeys(); + + @Query(value = "SELECT __key__ from test_entities_ci limit 1") + Key getKey(); + + @Query("SELECT id FROM test_entities_ci WHERE id <= @id_val") + List<String> getIds(@Param("id_val") long id); + + @Query("SELECT id FROM test_entities_ci WHERE id <= @id_val limit 1") + String getOneId(@Param("id_val") long id); +SpEL can be used to provide GQL parameters: +@Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act + AND price > :#{#priceRadius * -1} AND price < :#{#priceRadius * 2}") +List<Trade> fetchByActionNamedQuery(@Param("act") String action, @Param("priceRadius") Double r); +Kind names can be directly written in the GQL annotations. +Kind names can also be resolved from the @Entity annotation on domain classes. +In this case, the query should refer to table names with fully qualified class names surrounded by | characters: |fully.qualified.ClassName|. +This is useful when SpEL expressions appear in the kind name provided to the @Entity annotation. +For example: +@Query("SELECT * FROM |com.example.Trade| WHERE trades.action = @act") +List<Trade> fetchByActionNamedQuery(@Param("act") String action); +
    +
    +Query methods with named queries properties +You can also specify queries with Cloud Datastore parameter tags and SpEL expressions in properties files. +By default, the namedQueriesLocation attribute on @EnableDatastoreRepositories points to the META-INF/datastore-named-queries.properties file. +You can specify the query for a method in the properties file by providing the GQL as the value for the "interface.method" property: +Trader.fetchByName=SELECT * FROM traders WHERE name = @tag0 +public interface TraderRepository extends DatastoreRepository<Trader, String> { + + // This method uses the query from the properties file instead of one generated based on name. + List<Trader> fetchByName(@Param("tag0") String traderName); + +} +
    +
    +
    +Transactions +These transactions work very similarly to those of DatastoreOperations, but is specific to the repository’s domain type and provides repository functions instead of template functions. +For example, this is a read-write transaction: +@Autowired +DatastoreRepository myRepo; + +public String doWorkInsideTransaction() { + return myRepo.performTransaction( + transactionDatastoreRepo -> { + // Work with the single-transaction transactionDatastoreRepo here. + // This is a DatastoreRepository object. + + return "transaction completed"; + } + ); +} +
    +
    +Projections +Spring Data Cloud Datastore supports projections. +You can define projection interfaces based on domain types and add query methods that return them in your repository: +public interface TradeProjection { + + String getAction(); + + @Value("#{target.symbol + ' ' + target.action}") + String getSymbolAndAction(); +} + +public interface TradeRepository extends DatastoreRepository<Trade, Key> { + + List<Trade> findByTraderId(String traderId); + + List<TradeProjection> findByAction(String action); + + @Query("SELECT action, symbol FROM trades WHERE action = @action") + List<TradeProjection> findByQuery(String action); +} +Projections can be provided by name-convention-based query methods as well as by custom GQL queries. +If using custom GQL queries, you can further restrict the fields retrieved from Cloud Datastore to just those required by the projection. +However, custom select statements (those not using SELECT *) require composite indexes containing the selected fields. +Properties of projection types defined using SpEL use the fixed name target for the underlying domain object. +As a result, accessing underlying properties take the form target.<property-name>. +
    +
    +REST Repositories +When running with Spring Boot, repositories can be exposed as REST services by simply adding this dependency to your pom file: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-rest</artifactId> +</dependency> +If you prefer to configure parameters (such as path), you can use @RepositoryRestResource annotation: +@RepositoryRestResource(collectionResourceRel = "trades", path = "trades") +public interface TradeRepository extends DatastoreRepository<Trade, String[]> { +} +For example, you can retrieve all Trade objects in the repository by using curl http://<server>:<port>/trades, or any specific trade via curl http://<server>:<port>/trades/<trader_id>. +You can also write trades using curl -XPOST -H"Content-Type: application/json" -d@test.json http://<server>:<port>/trades/ where the file test.json holds the JSON representation of a Trade object. +To delete trades, you can use curl -XDELETE http://<server>:<port>/trades/<trader_id> +
    +
    +
    +Sample +A Simple Spring Boot Application and more advanced Sample Spring Boot Application are provided to show how to use the Spring Data Cloud Datastore starter and template. +
    +
    + +Cloud Memorystore for Redis +
    +Spring Caching +Cloud Memorystore for Redis provides a fully managed in-memory data store service. +Cloud Memorystore is compatible with the Redis protocol, allowing easy integration with Spring Caching. +All you have to do is create a Cloud Memorystore instance and use its IP address in application.properties file as spring.redis.host property value. +Everything else is exactly the same as setting up redis-backed Spring caching. + +Memorystore instances and your application instances have to be located in the same region. + +In short, the following dependencies are needed: +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-cache</artifactId> +</dependency> +<dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-redis</artifactId> +</dependency> +And then you can use org.springframework.cache.annotation.Cacheable annotation for methods you’d like to be cached. +@Cacheable("cache1") +public String hello(@PathVariable String name) { + .... +} +If you are interested in a detailed how-to guide, please check Spring Boot Caching using Cloud Memorystore codelab. +Cloud Memorystore documentation can be found here. +
    +
    + +Cloud Identity-Aware Proxy (IAP) Authentication +Cloud Identity-Aware Proxy (IAP) provides a security layer over applications deployed to Google Cloud. +The IAP starter uses Spring Security OAuth 2.0 Resource Server functionality to automatically extract user identity from the proxy-injected x-goog-iap-jwt-assertion HTTP header. +The following claims are validated automatically: + + +Issue time + + +Expiration time + + +Issuer + + +Audience + + +The audience ("aud") validation is automatically configured when the application is running on App Engine Standard or App Engine Flexible. +For other runtime environments, a custom audience must be provided through spring.cloud.gcp.security.iap.audience property. +The custom property, if specified, overrides the automatic App Engine audience detection. + +There is no automatic audience string configuration for Compute Engine or Kubernetes Engine. +To use the IAP starter on GCE/GKE, find the Audience string per instructions in the Verify the JWT payload guide, and specify it in the spring.cloud.gcp.security.iap.audience property. +Otherwise, the application will fail to start with No qualifying bean of type 'org.springframework.cloud.gcp.security.iap.AudienceProvider' available message. + + +If you create a custom WebSecurityConfigurerAdapter, enable extracting user identity by adding .oauth2ResourceServer().jwt() configuration to the HttpSecurity object. + If no custom WebSecurityConfigurerAdapter is present, nothing needs to be done because Spring Boot will add this customization by default. + +Starter Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-security-iap</artifactId> +</dependency> +Starter Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-security-iap' +} +
    +Configuration +The following properties are available. + +Modifying registry, algorithm, and header properties might be useful for testing, but the defaults should not be changed in production. + + + + + + + + + +Name +Description +Required +Default + + + + +spring.cloud.gcp.security.iap.registry +Link to JWK public key registry. +true +https://www.gstatic.com/iap/verify/public_key-jwk + + +spring.cloud.gcp.security.iap.algorithm +Encryption algorithm used to sign the JWK token. +true +ES256 + + +spring.cloud.gcp.security.iap.header +Header from which to extract the JWK key. +true +x-goog-iap-jwt-assertion + + +spring.cloud.gcp.security.iap.issuer +JWK issuer to verify. +true +https://cloud.google.com/iap + + +spring.cloud.gcp.security.iap.audience +Custom JWK audience to verify. +false on App Engine; true on GCE/GKE + + + + + +
    +
    +Sample +A sample application is available. +
    +
    + +Google Cloud Vision +The Google Cloud Vision API allows users to leverage machine learning algorithms for processing images including: image classification, face detection, text extraction, and others. +Spring Cloud GCP provides: + + +A convenience starter which automatically configures authentication settings and client objects needed to begin using the Google Cloud Vision API. + + +A Cloud Vision Template which simplifies interactions with the Cloud Vision API. + + +Allows you to easily send images to the API as Spring Resources. + + +Offers convenience methods for common operations, such as extracting the text from an image. + + + + +Maven coordinates, using Spring Cloud GCP BOM: +<dependency> + <groupId>org.springframework.cloud</groupId> + <artifactId>spring-cloud-gcp-starter-vision</artifactId> +</dependency> +Gradle coordinates: +dependencies { + compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-vision' +} +
    +Cloud Vision Template +The CloudVisionTemplate offers a simple way to use the Cloud Vision APIs with Spring Resources. +After you add the spring-cloud-gcp-starter-vision dependency to your project, you may @Autowire an instance of CloudVisionTemplate to use in your code. +The CloudVisionTemplate offers the following method for interfacing with Cloud Vision: +public AnnotateImageResponse analyzeImage(Resource imageResource, Feature.Type…​ featureTypes) +Parameters: + + +Resource imageResource refers to the Spring Resource of the image object you wish to analyze. +The Google Cloud Vision documentation provides a list of the image types that they support. + + +Feature.Type…​ featureTypes refers to a var-arg array of Cloud Vision Features to extract from the image. +A feature refers to a kind of image analysis one wishes to perform on an image, such as label detection, OCR recognition, facial detection, etc. +One may specify multiple features to analyze within one request. +A full list of Cloud Vision Features is provided in the Cloud Vision Feature docs. + + +Returns: + + +AnnotateImageResponse contains the results of all the feature analyses that were specified in the request. +For each feature type that you provide in the request, AnnotateImageResponse provides a getter method to get the result of that feature analysis. +For example, if you analyzed an image using the LABEL_DETECTION feature, you would retrieve the results from the response using annotateImageResponse.getLabelAnnotationsList(). +AnnotateImageResponse is provided by the Google Cloud Vision libraries; please consult the RPC reference or Javadoc for more details. +Additionally, you may consult the Cloud Vision docs to familiarize yourself with the concepts and features of the API. + + +
    +
    +Detect Image Labels Example +Image labeling refers to producing labels that describe the contents of an image. +Below is a code sample of how this is done using the Cloud Vision Spring Template. +@Autowired +private ResourceLoader resourceLoader; + +@Autowired +private CloudVisionTemplate cloudVisionTemplate; + +public void processImage() { + Resource imageResource = this.resourceLoader.getResource("my_image.jpg"); + AnnotateImageResponse response = this.cloudVisionTemplate.analyzeImage( + imageResource, Type.LABEL_DETECTION); + System.out.println("Image Classification results: " + response.getLabelAnnotationsList()); +} +
    +
    +Sample +A Sample Spring Boot Application is provided to show how to use the Cloud Vision starter and template. +
    +
    + +Cloud Foundry +Spring Cloud GCP provides support for Cloud Foundry’s GCP Service Broker. +Our Pub/Sub, Cloud Spanner, Storage, Stackdriver Trace and Cloud SQL MySQL and PostgreSQL starters are Cloud Foundry aware and retrieve properties like project ID, credentials, etc., that are used in auto configuration from the Cloud Foundry environment. +In cases like Pub/Sub’s topic and subscription, or Storage’s bucket name, where those parameters are not used in auto configuration, you can fetch them using the VCAP mapping provided by Spring Boot. +For example, to retrieve the provisioned Pub/Sub topic, you can use the vcap.services.mypubsub.credentials.topic_name property from the application environment. + +If the same service is bound to the same application more than once, the auto configuration will not be able to choose among bindings and will not be activated for that service. +This includes both MySQL and PostgreSQL bindings to the same app. + + +In order for the Cloud SQL integration to work in Cloud Foundry, auto-reconfiguration must be disabled. +You can do so using the cf set-env <APP> JBP_CONFIG_SPRING_AUTO_RECONFIGURATION '{enabled: false}' command. +Otherwise, Cloud Foundry will produce a DataSource with an invalid JDBC URL (i.e., jdbc:mysql://null/null). + + + +Kotlin Support +The latest version of the Spring Framework provides first-class support for Kotlin. +For Kotlin users of Spring, the Spring Cloud GCP libraries work out-of-the-box and are fully interoperable with Kotlin applications. +For more information on building a Spring application in Kotlin, please consult the Spring Kotlin documentation. +
    +Prerequisites +Ensure that your Kotlin application is properly set up. +Based on your build system, you will need to include the correct Kotlin build plugin in your project: + + +Kotlin Maven Plugin + + +Kotlin Gradle Plugin + + +Depending on your application’s needs, you may need to augment your build configuration with compiler plugins: + + +Kotlin Spring Plugin: Makes your Spring configuration classes/members non-final for convenience. + + +Kotlin JPA Plugin: Enables using JPA in Kotlin applications. + + +Once your Kotlin project is properly configured, the Spring Cloud GCP libraries will work within your application without any additional setup. +
    +
    + +Sample +A Kotlin sample application is provided to demonstrate a working Maven setup and various Spring Cloud GCP integrations from within Kotlin. + +
    + +Appendix: Compendium of Configuration Properties + + + + + + + + +Name +Default +Description + + +aws.paramstore.default-context +application + + + +aws.paramstore.enabled +true +Is AWS Parameter Store support enabled. + + +aws.paramstore.fail-fast +true +Throw exceptions during config lookup if true, otherwise, log warnings. + + +aws.paramstore.name + +Alternative to spring.application.name to use in looking up values in AWS Parameter Store. + + +aws.paramstore.prefix +/config +Prefix indicating first level for every property. Value must start with a forward slash followed by a valid path segment or be empty. Defaults to "/config". + + +aws.paramstore.profile-separator +_ + + + +cloud.aws.credentials.access-key + +The access key to be used with a static provider. + + +cloud.aws.credentials.instance-profile +true +Configures an instance profile credentials provider with no further configuration. + + +cloud.aws.credentials.profile-name + +The AWS profile name. + + +cloud.aws.credentials.profile-path + +The AWS profile path. + + +cloud.aws.credentials.secret-key + +The secret key to be used with a static provider. + + +cloud.aws.credentials.use-default-aws-credentials-chain +false +Use the DefaultAWSCredentials Chain instead of configuring a custom credentials chain. + + +cloud.aws.loader.core-pool-size +1 +The core pool size of the Task Executor used for parallel S3 interaction. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setCorePoolSize(int) + + +cloud.aws.loader.max-pool-size + +The maximum pool size of the Task Executor used for parallel S3 interaction. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize(int) + + +cloud.aws.loader.queue-capacity + +The maximum queue capacity for backed up S3 requests. @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setQueueCapacity(int) + + +cloud.aws.region.auto +true +Enables automatic region detection based on the EC2 meta data service. + + +cloud.aws.region.static + + + + +cloud.aws.stack.auto +true +Enables the automatic stack name detection for the application. + + +cloud.aws.stack.name +myStackName +The name of the manually configured stack name that will be used to retrieve the resources. + + +encrypt.fail-on-error +true +Flag to say that a process should fail if there is an encryption or decryption error. + + +encrypt.key + +A symmetric key. As a stronger alternative, consider using a keystore. + + +encrypt.key-store.alias + +Alias for a key in the store. + + +encrypt.key-store.location + +Location of the key store file, e.g. classpath:/keystore.jks. + + +encrypt.key-store.password + +Password that locks the keystore. + + +encrypt.key-store.secret + +Secret protecting the key (defaults to the same as the password). + + +encrypt.key-store.type +jks +The KeyStore type. Defaults to jks. + + +encrypt.rsa.algorithm + +The RSA algorithm to use (DEFAULT or OEAP). Once it is set, do not change it (or existing ciphers will not be decryptable). + + +encrypt.rsa.salt +deadbeef +Salt for the random secret used to encrypt cipher text. Once it is set, do not change it (or existing ciphers will not be decryptable). + + +encrypt.rsa.strong +false +Flag to indicate that "strong" AES encryption should be used internally. If true, then the GCM algorithm is applied to the AES encrypted bytes. Default is false (in which case "standard" CBC is used instead). Once it is set, do not change it (or existing ciphers will not be decryptable). + + +encrypt.salt +deadbeef +A salt for the symmetric key, in the form of a hex-encoded byte array. As a stronger alternative, consider using a keystore. + + +endpoints.zookeeper.enabled +true +Enable the /zookeeper endpoint to inspect the state of zookeeper. + + +eureka.client.healthcheck.enabled +true +Enables the Eureka health check handler. + + +health.config.enabled +false +Flag to indicate that the config server health indicator should be installed. + + +health.config.time-to-live +0 +Time to live for cached result, in milliseconds. Default 300000 (5 min). + + +hystrix.metrics.enabled +true +Enable Hystrix metrics polling. Defaults to true. + + +hystrix.metrics.polling-interval-ms +2000 +Interval between subsequent polling of metrics. Defaults to 2000 ms. + + +hystrix.shareSecurityContext +false +Enables auto-configuration of the Hystrix concurrency strategy plugin hook who will transfer the SecurityContext from your main thread to the one used by the Hystrix command. + + +management.endpoint.bindings.cache.time-to-live +0ms +Maximum time that a response can be cached. + + +management.endpoint.bindings.enabled +true +Whether to enable the bindings endpoint. + + +management.endpoint.bus-env.enabled +true +Whether to enable the bus-env endpoint. + + +management.endpoint.bus-refresh.enabled +true +Whether to enable the bus-refresh endpoint. + + +management.endpoint.channels.cache.time-to-live +0ms +Maximum time that a response can be cached. + + +management.endpoint.channels.enabled +true +Whether to enable the channels endpoint. + + +management.endpoint.consul.cache.time-to-live +0ms +Maximum time that a response can be cached. + + +management.endpoint.consul.enabled +true +Whether to enable the consul endpoint. + + +management.endpoint.env.post.enabled +true +Enables writable environment endpoint. + + +management.endpoint.features.cache.time-to-live +0ms +Maximum time that a response can be cached. + + +management.endpoint.features.enabled +true +Whether to enable the features endpoint. + + +management.endpoint.gateway.enabled +true +Whether to enable the gateway endpoint. + + +management.endpoint.hystrix.config + +Hystrix settings. These are traditionally set using servlet parameters. Refer to the documentation of Hystrix for more details. + + +management.endpoint.hystrix.stream.enabled +true +Whether to enable the hystrix.stream endpoint. + + +management.endpoint.pause.enabled +true +Enable the /pause endpoint (to send Lifecycle.stop()). + + +management.endpoint.refresh.enabled +true +Enable the /refresh endpoint to refresh configuration and re-initialize refresh scoped beans. + + +management.endpoint.restart.enabled +true +Enable the /restart endpoint to restart the application context. + + +management.endpoint.resume.enabled +true +Enable the /resume endpoint (to send Lifecycle.start()). + + +management.endpoint.service-registry.cache.time-to-live +0ms +Maximum time that a response can be cached. + + +management.endpoint.service-registry.enabled +true +Whether to enable the service-registry endpoint. + + +management.health.binders.enabled +true +Allows to enable/disable binder’s' health indicators. If you want to disable health indicator completely, then set it to false. + + +management.health.refresh.enabled +true +Enable the health endpoint for the refresh scope. + + +management.health.zookeeper.enabled +true +Enable the health endpoint for zookeeper. + + +management.metrics.binders.hystrix.enabled +true +Enables creation of OK Http Client factory beans. + + +management.metrics.export.cloudwatch.batch-size + + + + +management.metrics.export.cloudwatch.connect-timeout + + + + +management.metrics.export.cloudwatch.enabled +true +Enables cloud watch metrics. + + +management.metrics.export.cloudwatch.namespace + +Cloud watch namespace. + + +management.metrics.export.cloudwatch.num-threads + + + + +management.metrics.export.cloudwatch.read-timeout + + + + +management.metrics.export.cloudwatch.step + + + + +maven.checksum-policy + + + + +maven.connect-timeout + + + + +maven.enable-repository-listener + + + + +maven.local-repository + + + + +maven.offline + + + + +maven.proxy + + + + +maven.remote-repositories + + + + +maven.request-timeout + + + + +maven.resolve-pom + + + + +maven.update-policy + + + + +proxy.auth.load-balanced +false + + + +proxy.auth.routes + +Authentication strategy per route. + + +ribbon.eager-load.clients + + + + +ribbon.eager-load.enabled +false + + + +ribbon.http.client.enabled +false +Deprecated property to enable Ribbon RestClient. + + +ribbon.okhttp.enabled +false +Enables the use of the OK HTTP Client with Ribbon. + + +ribbon.restclient.enabled +false +Enables the use of the deprecated Ribbon RestClient. + + +ribbon.secure-ports + + + + +spring.cloud.bus.ack.destination-service + +Service that wants to listen to acks. By default null (meaning all services). + + +spring.cloud.bus.ack.enabled +true +Flag to switch off acks (default on). + + +spring.cloud.bus.destination +springCloudBus +Name of Spring Cloud Stream destination for messages. + + +spring.cloud.bus.enabled +true +Flag to indicate that the bus is enabled. + + +spring.cloud.bus.env.enabled +true +Flag to switch off environment change events (default on). + + +spring.cloud.bus.id +application +The identifier for this application instance. + + +spring.cloud.bus.refresh.enabled +true +Flag to switch off refresh events (default on). + + +spring.cloud.bus.trace.enabled +false +Flag to switch on tracing of acks (default off). + + +spring.cloud.cloudfoundry.discovery.default-server-port +80 +Port to use when no port is defined by ribbon. + + +spring.cloud.cloudfoundry.discovery.enabled +true +Flag to indicate that discovery is enabled. + + +spring.cloud.cloudfoundry.discovery.heartbeat-frequency +5000 +Frequency in milliseconds of poll for heart beat. The client will poll on this frequency and broadcast a list of service ids. + + +spring.cloud.cloudfoundry.discovery.order +0 +Order of the discovery client used by CompositeDiscoveryClient for sorting available clients. + + +spring.cloud.cloudfoundry.org + +Organization name to initially target. + + +spring.cloud.cloudfoundry.password + +Password for user to authenticate and obtain token. + + +spring.cloud.cloudfoundry.skip-ssl-validation +false + + + +spring.cloud.cloudfoundry.space + +Space name to initially target. + + +spring.cloud.cloudfoundry.url + +URL of Cloud Foundry API (Cloud Controller). + + +spring.cloud.cloudfoundry.username + +Username to authenticate (usually an email address). + + +spring.cloud.compatibility-verifier.compatible-boot-versions +2.1.x +Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don’t want to specify a concrete value. Example: {@code 3.4.x} + + +spring.cloud.compatibility-verifier.enabled +false +Enables creation of Spring Cloud compatibility verification. + + +spring.cloud.config.allow-override +true +Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true. + + +spring.cloud.config.discovery.enabled +false +Flag to indicate that config server discovery is enabled (config server URL will be looked up via discovery). + + +spring.cloud.config.discovery.service-id +configserver +Service id to locate config server. + + +spring.cloud.config.enabled +true +Flag to say that remote configuration is enabled. Default true; + + +spring.cloud.config.fail-fast +false +Flag to indicate that failure to connect to the server is fatal (default false). + + +spring.cloud.config.headers + +Additional headers used to create the client request. + + +spring.cloud.config.label + +The label name to use to pull remote configuration properties. The default is set on the server (generally "master" for a git based server). + + +spring.cloud.config.name + +Name of application used to fetch remote properties. + + +spring.cloud.config.override-none +false +Flag to indicate that when {@link #setAllowOverride(boolean) allowOverride} is true, external properties should take lowest priority and should not override any existing property sources (including local config files). Default false. + + +spring.cloud.config.override-system-properties +true +Flag to indicate that the external properties should override system properties. Default true. + + +spring.cloud.config.password + +The password to use (HTTP Basic) when contacting the remote server. + + +spring.cloud.config.profile +default +The default profile to use when fetching remote configuration (comma-separated). Default is "default". + + +spring.cloud.config.request-connect-timeout +0 +timeout on waiting to connect to the Config Server. + + +spring.cloud.config.request-read-timeout +0 +timeout on waiting to read data from the Config Server. + + +spring.cloud.config.retry.initial-interval +1000 +Initial retry interval in milliseconds. + + +spring.cloud.config.retry.max-attempts +6 +Maximum number of attempts. + + +spring.cloud.config.retry.max-interval +2000 +Maximum interval for backoff. + + +spring.cloud.config.retry.multiplier +1.1 +Multiplier for next interval. + + +spring.cloud.config.send-state +true +Flag to indicate whether to send state. Default true. + + +spring.cloud.config.server.accept-empty +true +Flag to indicate that If HTTP 404 needs to be sent if Application is not Found. + + +spring.cloud.config.server.bootstrap +false +Flag indicating that the config server should initialize its own Environment with properties from the remote repository. Off by default because it delays startup but can be useful when embedding the server in another application. + + +spring.cloud.config.server.credhub.ca-cert-files + + + + +spring.cloud.config.server.credhub.connection-timeout + + + + +spring.cloud.config.server.credhub.oauth2.registration-id + + + + +spring.cloud.config.server.credhub.order + + + + +spring.cloud.config.server.credhub.read-timeout + + + + +spring.cloud.config.server.credhub.url + + + + +spring.cloud.config.server.default-application-name +application +Default application name when incoming requests do not have a specific one. + + +spring.cloud.config.server.default-label + +Default repository label when incoming requests do not have a specific label. + + +spring.cloud.config.server.default-profile +default +Default application profile when incoming requests do not have a specific one. + + +spring.cloud.config.server.encrypt.enabled +true +Enable decryption of environment properties before sending to client. + + +spring.cloud.config.server.git.basedir + +Base directory for local working copy of repository. + + +spring.cloud.config.server.git.clone-on-start +false +Flag to indicate that the repository should be cloned on startup (not on demand). Generally leads to slower startup but faster first query. + + +spring.cloud.config.server.git.default-label + +The default label to be used with the remote repository. + + +spring.cloud.config.server.git.delete-untracked-branches +false +Flag to indicate that the branch should be deleted locally if it’s origin tracked branch was removed. + + +spring.cloud.config.server.git.force-pull +false +Flag to indicate that the repository should force pull. If true discard any local changes and take from remote repository. + + +spring.cloud.config.server.git.host-key + +Valid SSH host key. Must be set if hostKeyAlgorithm is also set. + + +spring.cloud.config.server.git.host-key-algorithm + +One of ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521. Must be set if hostKey is also set. + + +spring.cloud.config.server.git.ignore-local-ssh-settings +false +If true, use property-based instead of file-based SSH config. + + +spring.cloud.config.server.git.known-hosts-file + +Location of custom .known_hosts file. + + +spring.cloud.config.server.git.order + +The order of the environment repository. + + +spring.cloud.config.server.git.passphrase + +Passphrase for unlocking your ssh private key. + + +spring.cloud.config.server.git.password + +Password for authentication with remote repository. + + +spring.cloud.config.server.git.preferred-authentications + +Override server authentication method order. This should allow for evading login prompts if server has keyboard-interactive authentication before the publickey method. + + +spring.cloud.config.server.git.private-key + +Valid SSH private key. Must be set if ignoreLocalSshSettings is true and Git URI is SSH format. + + +spring.cloud.config.server.git.proxy + +HTTP proxy configuration. + + +spring.cloud.config.server.git.refresh-rate +0 +Time (in seconds) between refresh of the git repository. + + +spring.cloud.config.server.git.repos + +Map of repository identifier to location and other properties. + + +spring.cloud.config.server.git.search-paths + +Search paths to use within local working copy. By default searches only the root. + + +spring.cloud.config.server.git.skip-ssl-validation +false +Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served over an HTTPS connection. + + +spring.cloud.config.server.git.strict-host-key-checking +true +If false, ignore errors with host key. + + +spring.cloud.config.server.git.timeout +5 +Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable), defaults to 5 seconds. + + +spring.cloud.config.server.git.uri + +URI of remote repository. + + +spring.cloud.config.server.git.username + +Username for authentication with remote repository. + + +spring.cloud.config.server.health.repositories + + + + +spring.cloud.config.server.jdbc.order +0 + + + +spring.cloud.config.server.jdbc.sql +SELECT KEY, VALUE from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=? +SQL used to query database for keys and values. + + +spring.cloud.config.server.native.add-label-locations +true +Flag to determine whether label locations should be added. + + +spring.cloud.config.server.native.default-label +master + + + +spring.cloud.config.server.native.fail-on-error +false +Flag to determine how to handle exceptions during decryption (default false). + + +spring.cloud.config.server.native.order + + + + +spring.cloud.config.server.native.search-locations +[] +Locations to search for configuration files. Defaults to the same as a Spring Boot app so [classpath:/,classpath:/config/,file:./,file:./config/]. + + +spring.cloud.config.server.native.version + +Version string to be reported for native repository. + + +spring.cloud.config.server.overrides + +Extra map for a property source to be sent to all clients unconditionally. + + +spring.cloud.config.server.prefix + +Prefix for configuration resource paths (default is empty). Useful when embedding in another application when you don’t want to change the context path or servlet path. + + +spring.cloud.config.server.strip-document-from-yaml +true +Flag to indicate that YAML documents that are text or collections (not a map) should be returned in "native" form. + + +spring.cloud.config.server.svn.basedir + +Base directory for local working copy of repository. + + +spring.cloud.config.server.svn.default-label + +The default label to be used with the remote repository. + + +spring.cloud.config.server.svn.order + +The order of the environment repository. + + +spring.cloud.config.server.svn.passphrase + +Passphrase for unlocking your ssh private key. + + +spring.cloud.config.server.svn.password + +Password for authentication with remote repository. + + +spring.cloud.config.server.svn.search-paths + +Search paths to use within local working copy. By default searches only the root. + + +spring.cloud.config.server.svn.strict-host-key-checking +true +Reject incoming SSH host keys from remote servers not in the known host list. + + +spring.cloud.config.server.svn.uri + +URI of remote repository. + + +spring.cloud.config.server.svn.username + +Username for authentication with remote repository. + + +spring.cloud.config.server.vault.backend +secret +Vault backend. Defaults to secret. + + +spring.cloud.config.server.vault.default-key +application +The key in vault shared by all applications. Defaults to application. Set to empty to disable. + + +spring.cloud.config.server.vault.host +127.0.0.1 +Vault host. Defaults to 127.0.0.1. + + +spring.cloud.config.server.vault.kv-version +1 +Value to indicate which version of Vault kv backend is used. Defaults to 1. + + +spring.cloud.config.server.vault.namespace + +The value of the Vault X-Vault-Namespace header. Defaults to null. This a Vault Enterprise feature only. + + +spring.cloud.config.server.vault.order + + + + +spring.cloud.config.server.vault.port +8200 +Vault port. Defaults to 8200. + + +spring.cloud.config.server.vault.profile-separator +, +Vault profile separator. Defaults to comma. + + +spring.cloud.config.server.vault.proxy + +HTTP proxy configuration. + + +spring.cloud.config.server.vault.scheme +http +Vault scheme. Defaults to http. + + +spring.cloud.config.server.vault.skip-ssl-validation +false +Flag to indicate that SSL certificate validation should be bypassed when communicating with a repository served over an HTTPS connection. + + +spring.cloud.config.server.vault.timeout +5 +Timeout (in seconds) for obtaining HTTP connection, defaults to 5 seconds. + + +spring.cloud.config.token + +Security Token passed thru to underlying environment repository. + + +spring.cloud.config.uri +[http://localhost:8888] +The URI of the remote server (default http://localhost:8888). + + +spring.cloud.config.username + +The username to use (HTTP Basic) when contacting the remote server. + + +spring.cloud.consul.config.acl-token + + + + +spring.cloud.consul.config.data-key +data +If format is Format.PROPERTIES or Format.YAML then the following field is used as key to look up consul for configuration. + + +spring.cloud.consul.config.default-context +application + + + +spring.cloud.consul.config.enabled +true + + + +spring.cloud.consul.config.fail-fast +true +Throw exceptions during config lookup if true, otherwise, log warnings. + + +spring.cloud.consul.config.format + + + + +spring.cloud.consul.config.name + +Alternative to spring.application.name to use in looking up values in consul KV. + + +spring.cloud.consul.config.prefix +config + + + +spring.cloud.consul.config.profile-separator +, + + + +spring.cloud.consul.config.watch.delay +1000 +The value of the fixed delay for the watch in millis. Defaults to 1000. + + +spring.cloud.consul.config.watch.enabled +true +If the watch is enabled. Defaults to true. + + +spring.cloud.consul.config.watch.wait-time +55 +The number of seconds to wait (or block) for watch query, defaults to 55. Needs to be less than default ConsulClient (defaults to 60). To increase ConsulClient timeout create a ConsulClient bean with a custom ConsulRawClient with a custom HttpClient. + + +spring.cloud.consul.discovery.acl-token + + + + +spring.cloud.consul.discovery.catalog-services-watch-delay +1000 +The delay between calls to watch consul catalog in millis, default is 1000. + + +spring.cloud.consul.discovery.catalog-services-watch-timeout +2 +The number of seconds to block while watching consul catalog, default is 2. + + +spring.cloud.consul.discovery.datacenters + +Map of serviceId’s → datacenter to query for in server list. This allows looking up services in another datacenters. + + +spring.cloud.consul.discovery.default-query-tag + +Tag to query for in service list if one is not listed in serverListQueryTags. + + +spring.cloud.consul.discovery.default-zone-metadata-name +zone +Service instance zone comes from metadata. This allows changing the metadata tag name. + + +spring.cloud.consul.discovery.deregister +true +Disable automatic de-registration of service in consul. + + +spring.cloud.consul.discovery.enabled +true +Is service discovery enabled? + + +spring.cloud.consul.discovery.fail-fast +true +Throw exceptions during service registration if true, otherwise, log warnings (defaults to true). + + +spring.cloud.consul.discovery.health-check-critical-timeout + +Timeout to deregister services critical for longer than timeout (e.g. 30m). Requires consul version 7.x or higher. + + +spring.cloud.consul.discovery.health-check-headers + +Headers to be applied to the Health Check calls. + + +spring.cloud.consul.discovery.health-check-interval +10s +How often to perform the health check (e.g. 10s), defaults to 10s. + + +spring.cloud.consul.discovery.health-check-path +/actuator/health +Alternate server path to invoke for health checking. + + +spring.cloud.consul.discovery.health-check-timeout + +Timeout for health check (e.g. 10s). + + +spring.cloud.consul.discovery.health-check-tls-skip-verify + +Skips certificate verification during service checks if true, otherwise runs certificate verification. + + +spring.cloud.consul.discovery.health-check-url + +Custom health check url to override default. + + +spring.cloud.consul.discovery.heartbeat.enabled +false + + + +spring.cloud.consul.discovery.heartbeat.interval-ratio + + + + +spring.cloud.consul.discovery.heartbeat.ttl-unit +s + + + +spring.cloud.consul.discovery.heartbeat.ttl-value +30 + + + +spring.cloud.consul.discovery.hostname + +Hostname to use when accessing server. + + +spring.cloud.consul.discovery.instance-group + +Service instance group. + + +spring.cloud.consul.discovery.instance-id + +Unique service instance id. + + +spring.cloud.consul.discovery.instance-zone + +Service instance zone. + + +spring.cloud.consul.discovery.ip-address + +IP address to use when accessing service (must also set preferIpAddress to use). + + +spring.cloud.consul.discovery.lifecycle.enabled +true + + + +spring.cloud.consul.discovery.management-port + +Port to register the management service under (defaults to management port). + + +spring.cloud.consul.discovery.management-suffix +management +Suffix to use when registering management service. + + +spring.cloud.consul.discovery.management-tags + +Tags to use when registering management service. + + +spring.cloud.consul.discovery.order +0 +Order of the discovery client used by CompositeDiscoveryClient for sorting available clients. + + +spring.cloud.consul.discovery.port + +Port to register the service under (defaults to listening port). + + +spring.cloud.consul.discovery.prefer-agent-address +false +Source of how we will determine the address to use. + + +spring.cloud.consul.discovery.prefer-ip-address +false +Use ip address rather than hostname during registration. + + +spring.cloud.consul.discovery.query-passing +false +Add the 'passing` parameter to /v1/health/service/serviceName. This pushes health check passing to the server. + + +spring.cloud.consul.discovery.register +true +Register as a service in consul. + + +spring.cloud.consul.discovery.register-health-check +true +Register health check in consul. Useful during development of a service. + + +spring.cloud.consul.discovery.scheme +http +Whether to register an http or https service. + + +spring.cloud.consul.discovery.server-list-query-tags + +Map of serviceId’s → tag to query for in server list. This allows filtering services by a single tag. + + +spring.cloud.consul.discovery.service-name + +Service name. + + +spring.cloud.consul.discovery.tags + +Tags to use when registering service. + + +spring.cloud.consul.enabled +true +Is spring cloud consul enabled. + + +spring.cloud.consul.host +localhost +Consul agent hostname. Defaults to 'localhost'. + + +spring.cloud.consul.port +8500 +Consul agent port. Defaults to '8500'. + + +spring.cloud.consul.retry.initial-interval +1000 +Initial retry interval in milliseconds. + + +spring.cloud.consul.retry.max-attempts +6 +Maximum number of attempts. + + +spring.cloud.consul.retry.max-interval +2000 +Maximum interval for backoff. + + +spring.cloud.consul.retry.multiplier +1.1 +Multiplier for next interval. + + +spring.cloud.consul.scheme + +Consul agent scheme (HTTP/HTTPS). If there is no scheme in address - client will use HTTP. + + +spring.cloud.consul.tls.certificate-password + +Password to open the certificate. + + +spring.cloud.consul.tls.certificate-path + +File path to the certificate. + + +spring.cloud.consul.tls.key-store-instance-type + +Type of key framework to use. + + +spring.cloud.consul.tls.key-store-password + +Password to an external keystore. + + +spring.cloud.consul.tls.key-store-path + +Path to an external keystore. + + +spring.cloud.discovery.client.cloudfoundry.order + + + + +spring.cloud.discovery.client.composite-indicator.enabled +true +Enables discovery client composite health indicator. + + +spring.cloud.discovery.client.health-indicator.enabled +true + + + +spring.cloud.discovery.client.health-indicator.include-description +false + + + +spring.cloud.discovery.client.simple.instances + + + + +spring.cloud.discovery.client.simple.local.instance-id + +The unique identifier or name for the service instance. + + +spring.cloud.discovery.client.simple.local.metadata + +Metadata for the service instance. Can be used by discovery clients to modify their behaviour per instance, e.g. when load balancing. + + +spring.cloud.discovery.client.simple.local.service-id + +The identifier or name for the service. Multiple instances might share the same service ID. + + +spring.cloud.discovery.client.simple.local.uri + +The URI of the service instance. Will be parsed to extract the scheme, host, and port. + + +spring.cloud.discovery.client.simple.order + + + + +spring.cloud.discovery.enabled +true +Enables discovery client health indicators. + + +spring.cloud.features.enabled +true +Enables the features endpoint. + + +spring.cloud.function.compile + +Configuration for function bodies, which will be compiled. The key in the map is the function name and the value is a map containing a key "lambda" which is the body to compile, and optionally a "type" (defaults to "function"). Can also contain "inputType" and "outputType" in case it is ambiguous. + + +spring.cloud.function.definition + +Name (e.g., 'foo') or composition instruction (e.g., 'foo|bar') used to resolve default function especially for cases where there is more then once function available in catalog. + + +spring.cloud.function.imports + +Configuration for a set of files containing function bodies, which will be imported and compiled. The key in the map is the function name and the value is another map, containing a "location" of the file to compile and (optionally) a "type" (defaults to "function"). + + +spring.cloud.function.scan.packages +functions +Triggers scanning within the specified base packages for any class that is assignable to java.util.function.Function. For each detected Function class, a bean instance will be added to the context. + + +spring.cloud.function.task.consumer + + + + +spring.cloud.function.task.function + + + + +spring.cloud.function.task.supplier + + + + +spring.cloud.function.web.path + +Path to web resources for functions (should start with / if not empty). + + +spring.cloud.function.web.supplier.auto-startup +true + + + +spring.cloud.function.web.supplier.debug +true + + + +spring.cloud.function.web.supplier.enabled +false + + + +spring.cloud.function.web.supplier.headers + + + + +spring.cloud.function.web.supplier.name + + + + +spring.cloud.function.web.supplier.template-url + + + + +spring.cloud.gateway.default-filters + +List of filter definitions that are applied to every route. + + +spring.cloud.gateway.discovery.locator.enabled +false +Flag that enables DiscoveryClient gateway integration. + + +spring.cloud.gateway.discovery.locator.filters + + + + +spring.cloud.gateway.discovery.locator.include-expression +true +SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true. + + +spring.cloud.gateway.discovery.locator.lower-case-service-id +false +Option to lower case serviceId in predicates and filters, defaults to false. Useful with eureka when it automatically uppercases serviceId. so MYSERIVCE, would match /myservice/** + + +spring.cloud.gateway.discovery.locator.predicates + + + + +spring.cloud.gateway.discovery.locator.route-id-prefix + +The prefix for the routeId, defaults to discoveryClient.getClass().getSimpleName() + "_". Service Id will be appended to create the routeId. + + +spring.cloud.gateway.discovery.locator.url-expression +'lb://'+serviceId +SpEL expression that create the uri for each route, defaults to: 'lb://'+serviceId. + + +spring.cloud.gateway.enabled +true +Enables gateway functionality. + + +spring.cloud.gateway.filter.remove-hop-by-hop.headers + + + + +spring.cloud.gateway.filter.remove-hop-by-hop.order + + + + +spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key +true +Switch to deny requests if the Key Resolver returns an empty key, defaults to true. + + +spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code + +HttpStatus to return when denyEmptyKey is true, defaults to FORBIDDEN. + + +spring.cloud.gateway.filter.secure-headers.content-security-policy +default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' + + + +spring.cloud.gateway.filter.secure-headers.content-type-options +nosniff + + + +spring.cloud.gateway.filter.secure-headers.disable + + + + +spring.cloud.gateway.filter.secure-headers.download-options +noopen + + + +spring.cloud.gateway.filter.secure-headers.frame-options +DENY + + + +spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies +none + + + +spring.cloud.gateway.filter.secure-headers.referrer-policy +no-referrer + + + +spring.cloud.gateway.filter.secure-headers.strict-transport-security +max-age=631138519 + + + +spring.cloud.gateway.filter.secure-headers.xss-protection-header +1 ; mode=block + + + +spring.cloud.gateway.forwarded.enabled +true +Enables the ForwardedHeadersFilter. + + +spring.cloud.gateway.globalcors.cors-configurations + + + + +spring.cloud.gateway.httpclient.connect-timeout + +The connect timeout in millis, the default is 45s. + + +spring.cloud.gateway.httpclient.max-header-size + +The max response header size. + + +spring.cloud.gateway.httpclient.pool.acquire-timeout + +Only for type FIXED, the maximum time in millis to wait for aquiring. + + +spring.cloud.gateway.httpclient.pool.max-connections + +Only for type FIXED, the maximum number of connections before starting pending acquisition on existing ones. + + +spring.cloud.gateway.httpclient.pool.name +proxy +The channel pool map name, defaults to proxy. + + +spring.cloud.gateway.httpclient.pool.type + +Type of pool for HttpClient to use, defaults to ELASTIC. + + +spring.cloud.gateway.httpclient.proxy.host + +Hostname for proxy configuration of Netty HttpClient. + + +spring.cloud.gateway.httpclient.proxy.non-proxy-hosts-pattern + +Regular expression (Java) for a configured list of hosts. that should be reached directly, bypassing the proxy + + +spring.cloud.gateway.httpclient.proxy.password + +Password for proxy configuration of Netty HttpClient. + + +spring.cloud.gateway.httpclient.proxy.port + +Port for proxy configuration of Netty HttpClient. + + +spring.cloud.gateway.httpclient.proxy.username + +Username for proxy configuration of Netty HttpClient. + + +spring.cloud.gateway.httpclient.response-timeout + +The response timeout. + + +spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout +3000ms +SSL close_notify flush timeout. Default to 3000 ms. + + +spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout-millis + + + + +spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout + +SSL close_notify read timeout. Default to 0 ms. + + +spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout-millis + + + + +spring.cloud.gateway.httpclient.ssl.default-configuration-type + +The default ssl configuration type. Defaults to TCP. + + +spring.cloud.gateway.httpclient.ssl.handshake-timeout +10000ms +SSL handshake timeout. Default to 10000 ms + + +spring.cloud.gateway.httpclient.ssl.handshake-timeout-millis + + + + +spring.cloud.gateway.httpclient.ssl.trusted-x509-certificates + +Trusted certificates for verifying the remote endpoint’s certificate. + + +spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager +false +Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production. + + +spring.cloud.gateway.httpclient.wiretap +false +Enables wiretap debugging for Netty HttpClient. + + +spring.cloud.gateway.httpserver.wiretap +false +Enables wiretap debugging for Netty HttpServer. + + +spring.cloud.gateway.loadbalancer.use404 +false + + + +spring.cloud.gateway.metrics.enabled +true +Enables the collection of metrics data. + + +spring.cloud.gateway.proxy.headers + +Fixed header values that will be added to all downstream requests. + + +spring.cloud.gateway.proxy.sensitive + +A set of sensitive header names that will not be sent downstream by default. + + +spring.cloud.gateway.redis-rate-limiter.burst-capacity-header +X-RateLimit-Burst-Capacity +The name of the header that returns the burst capacity configuration. + + +spring.cloud.gateway.redis-rate-limiter.config + + + + +spring.cloud.gateway.redis-rate-limiter.include-headers +true +Whether or not to include headers containing rate limiter information, defaults to true. + + +spring.cloud.gateway.redis-rate-limiter.remaining-header +X-RateLimit-Remaining +The name of the header that returns number of remaining requests during the current second. + + +spring.cloud.gateway.redis-rate-limiter.replenish-rate-header +X-RateLimit-Replenish-Rate +The name of the header that returns the replenish rate configuration. + + +spring.cloud.gateway.routes + +List of Routes. + + +spring.cloud.gateway.streaming-media-types + + + + +spring.cloud.gateway.x-forwarded.enabled +true +If the XForwardedHeadersFilter is enabled. + + +spring.cloud.gateway.x-forwarded.for-append +true +If appending X-Forwarded-For as a list is enabled. + + +spring.cloud.gateway.x-forwarded.for-enabled +true +If X-Forwarded-For is enabled. + + +spring.cloud.gateway.x-forwarded.host-append +true +If appending X-Forwarded-Host as a list is enabled. + + +spring.cloud.gateway.x-forwarded.host-enabled +true +If X-Forwarded-Host is enabled. + + +spring.cloud.gateway.x-forwarded.order +0 +The order of the XForwardedHeadersFilter. + + +spring.cloud.gateway.x-forwarded.port-append +true +If appending X-Forwarded-Port as a list is enabled. + + +spring.cloud.gateway.x-forwarded.port-enabled +true +If X-Forwarded-Port is enabled. + + +spring.cloud.gateway.x-forwarded.prefix-append +true +If appending X-Forwarded-Prefix as a list is enabled. + + +spring.cloud.gateway.x-forwarded.prefix-enabled +true +If X-Forwarded-Prefix is enabled. + + +spring.cloud.gateway.x-forwarded.proto-append +true +If appending X-Forwarded-Proto as a list is enabled. + + +spring.cloud.gateway.x-forwarded.proto-enabled +true +If X-Forwarded-Proto is enabled. + + +spring.cloud.gcp.config.credentials.encoded-key + + + + +spring.cloud.gcp.config.credentials.location + + + + +spring.cloud.gcp.config.credentials.scopes + + + + +spring.cloud.gcp.config.enabled +false +Enables Spring Cloud GCP Config. + + +spring.cloud.gcp.config.name + +Name of the application. + + +spring.cloud.gcp.config.profile + +Comma-delimited string of profiles under which the app is running. Gets its default value from the {@code spring.profiles.active} property, falling back on the {@code spring.profiles.default} property. + + +spring.cloud.gcp.config.project-id + +Overrides the GCP project ID specified in the Core module. + + +spring.cloud.gcp.config.timeout-millis +60000 +Timeout for Google Runtime Configuration API calls. + + +spring.cloud.gcp.credentials.encoded-key + + + + +spring.cloud.gcp.credentials.location + + + + +spring.cloud.gcp.credentials.scopes + + + + +spring.cloud.gcp.datastore.credentials.encoded-key + + + + +spring.cloud.gcp.datastore.credentials.location + + + + +spring.cloud.gcp.datastore.credentials.scopes + + + + +spring.cloud.gcp.datastore.namespace + + + + +spring.cloud.gcp.datastore.project-id + + + + +spring.cloud.gcp.logging.enabled +true +Auto-configure Google Cloud Stackdriver logging for Spring MVC. + + +spring.cloud.gcp.project-id + +GCP project ID where services are running. + + +spring.cloud.gcp.pubsub.credentials.encoded-key + + + + +spring.cloud.gcp.pubsub.credentials.location + + + + +spring.cloud.gcp.pubsub.credentials.scopes + + + + +spring.cloud.gcp.pubsub.emulator-host + +The host and port of the local running emulator. If provided, this will setup the client to connect against a running pub/sub emulator. + + +spring.cloud.gcp.pubsub.enabled +true +Auto-configure Google Cloud Pub/Sub components. + + +spring.cloud.gcp.pubsub.project-id + +Overrides the GCP project ID specified in the Core module. + + +spring.cloud.gcp.pubsub.publisher.batching.delay-threshold-seconds + +The delay threshold to use for batching. After this amount of time has elapsed (counting from the first element added), the elements will be wrapped up in a batch and sent. + + +spring.cloud.gcp.pubsub.publisher.batching.element-count-threshold + +The element count threshold to use for batching. + + +spring.cloud.gcp.pubsub.publisher.batching.enabled + +Enables batching if true. + + +spring.cloud.gcp.pubsub.publisher.batching.flow-control.limit-exceeded-behavior + +The behavior when the specified limits are exceeded. + + +spring.cloud.gcp.pubsub.publisher.batching.flow-control.max-outstanding-element-count + +Maximum number of outstanding elements to keep in memory before enforcing flow control. + + +spring.cloud.gcp.pubsub.publisher.batching.flow-control.max-outstanding-request-bytes + +Maximum number of outstanding bytes to keep in memory before enforcing flow control. + + +spring.cloud.gcp.pubsub.publisher.batching.request-byte-threshold + +The request byte threshold to use for batching. + + +spring.cloud.gcp.pubsub.publisher.executor-threads +4 +Number of threads used by every publisher. + + +spring.cloud.gcp.pubsub.publisher.retry.initial-retry-delay-seconds + +InitialRetryDelay controls the delay before the first retry. Subsequent retries will use this value adjusted according to the RetryDelayMultiplier. + + +spring.cloud.gcp.pubsub.publisher.retry.initial-rpc-timeout-seconds + +InitialRpcTimeout controls the timeout for the initial RPC. Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier. + + +spring.cloud.gcp.pubsub.publisher.retry.jittered + +Jitter determines if the delay time should be randomized. + + +spring.cloud.gcp.pubsub.publisher.retry.max-attempts + +MaxAttempts defines the maximum number of attempts to perform. If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout. + + +spring.cloud.gcp.pubsub.publisher.retry.max-retry-delay-seconds + +MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier can’t increase the retry delay higher than this amount. + + +spring.cloud.gcp.pubsub.publisher.retry.max-rpc-timeout-seconds + +MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier can’t increase the RPC timeout higher than this amount. + + +spring.cloud.gcp.pubsub.publisher.retry.retry-delay-multiplier + +RetryDelayMultiplier controls the change in retry delay. The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call. + + +spring.cloud.gcp.pubsub.publisher.retry.rpc-timeout-multiplier + +RpcTimeoutMultiplier controls the change in RPC timeout. The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call. + + +spring.cloud.gcp.pubsub.publisher.retry.total-timeout-seconds + +TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. The higher the total timeout, the more retries can be attempted. + + +spring.cloud.gcp.pubsub.subscriber.executor-threads +4 +Number of threads used by every subscriber. + + +spring.cloud.gcp.pubsub.subscriber.flow-control.limit-exceeded-behavior + +The behavior when the specified limits are exceeded. + + +spring.cloud.gcp.pubsub.subscriber.flow-control.max-outstanding-element-count + +Maximum number of outstanding elements to keep in memory before enforcing flow control. + + +spring.cloud.gcp.pubsub.subscriber.flow-control.max-outstanding-request-bytes + +Maximum number of outstanding bytes to keep in memory before enforcing flow control. + + +spring.cloud.gcp.pubsub.subscriber.max-ack-extension-period +0 +The optional max ack extension period in seconds for the subscriber factory. + + +spring.cloud.gcp.pubsub.subscriber.max-acknowledgement-threads +4 +Number of threads used for batch acknowledgement. + + +spring.cloud.gcp.pubsub.subscriber.parallel-pull-count + +The optional parallel pull count setting for the subscriber factory. + + +spring.cloud.gcp.pubsub.subscriber.pull-endpoint + +The optional pull endpoint setting for the subscriber factory. + + +spring.cloud.gcp.pubsub.subscriber.retry.initial-retry-delay-seconds + +InitialRetryDelay controls the delay before the first retry. Subsequent retries will use this value adjusted according to the RetryDelayMultiplier. + + +spring.cloud.gcp.pubsub.subscriber.retry.initial-rpc-timeout-seconds + +InitialRpcTimeout controls the timeout for the initial RPC. Subsequent calls will use this value adjusted according to the RpcTimeoutMultiplier. + + +spring.cloud.gcp.pubsub.subscriber.retry.jittered + +Jitter determines if the delay time should be randomized. + + +spring.cloud.gcp.pubsub.subscriber.retry.max-attempts + +MaxAttempts defines the maximum number of attempts to perform. If this value is greater than 0, and the number of attempts reaches this limit, the logic will give up retrying even if the total retry time is still lower than TotalTimeout. + + +spring.cloud.gcp.pubsub.subscriber.retry.max-retry-delay-seconds + +MaxRetryDelay puts a limit on the value of the retry delay, so that the RetryDelayMultiplier can’t increase the retry delay higher than this amount. + + +spring.cloud.gcp.pubsub.subscriber.retry.max-rpc-timeout-seconds + +MaxRpcTimeout puts a limit on the value of the RPC timeout, so that the RpcTimeoutMultiplier can’t increase the RPC timeout higher than this amount. + + +spring.cloud.gcp.pubsub.subscriber.retry.retry-delay-multiplier + +RetryDelayMultiplier controls the change in retry delay. The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call. + + +spring.cloud.gcp.pubsub.subscriber.retry.rpc-timeout-multiplier + +RpcTimeoutMultiplier controls the change in RPC timeout. The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call. + + +spring.cloud.gcp.pubsub.subscriber.retry.total-timeout-seconds + +TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. The higher the total timeout, the more retries can be attempted. + + +spring.cloud.gcp.security.iap.algorithm +ES256 +Encryption algorithm used to sign the JWK token. + + +spring.cloud.gcp.security.iap.audience + +Non-dynamic audience string to validate. + + +spring.cloud.gcp.security.iap.enabled +true +Auto-configure Google Cloud IAP identity extraction components. + + +spring.cloud.gcp.security.iap.header +x-goog-iap-jwt-assertion +Header from which to extract the JWK key. + + +spring.cloud.gcp.security.iap.issuer +https://cloud.google.com/iap +JWK issuer to verify. + + +spring.cloud.gcp.security.iap.registry +https://www.gstatic.com/iap/verify/public_key-jwk +Link to JWK public key registry. + + +spring.cloud.gcp.spanner.create-interleaved-table-ddl-on-delete-cascade +true + + + +spring.cloud.gcp.spanner.credentials.encoded-key + + + + +spring.cloud.gcp.spanner.credentials.location + + + + +spring.cloud.gcp.spanner.credentials.scopes + + + + +spring.cloud.gcp.spanner.database + + + + +spring.cloud.gcp.spanner.instance-id + + + + +spring.cloud.gcp.spanner.keep-alive-interval-minutes +-1 + + + +spring.cloud.gcp.spanner.max-idle-sessions +-1 + + + +spring.cloud.gcp.spanner.max-sessions +-1 + + + +spring.cloud.gcp.spanner.min-sessions +-1 + + + +spring.cloud.gcp.spanner.num-rpc-channels +-1 + + + +spring.cloud.gcp.spanner.prefetch-chunks +-1 + + + +spring.cloud.gcp.spanner.project-id + + + + +spring.cloud.gcp.spanner.write-sessions-fraction +-1 + + + +spring.cloud.gcp.sql.credentials + +Overrides the GCP OAuth2 credentials specified in the Core module. + + +spring.cloud.gcp.sql.database-name + +Name of the database in the Cloud SQL instance. + + +spring.cloud.gcp.sql.enabled +true +Auto-configure Google Cloud SQL support components. + + +spring.cloud.gcp.sql.instance-connection-name + +Cloud SQL instance connection name. [GCP_PROJECT_ID]:[INSTANCE_REGION]:[INSTANCE_NAME]. + + +spring.cloud.gcp.storage.auto-create-files + + + + +spring.cloud.gcp.storage.credentials.encoded-key + + + + +spring.cloud.gcp.storage.credentials.location + + + + +spring.cloud.gcp.storage.credentials.scopes + + + + +spring.cloud.gcp.storage.enabled +true +Auto-configure Google Cloud Storage components. + + +spring.cloud.gcp.trace.authority + +HTTP/2 authority the channel claims to be connecting to. + + +spring.cloud.gcp.trace.compression + +Compression to use for the call. + + +spring.cloud.gcp.trace.credentials.encoded-key + + + + +spring.cloud.gcp.trace.credentials.location + + + + +spring.cloud.gcp.trace.credentials.scopes + + + + +spring.cloud.gcp.trace.deadline-ms + +Call deadline. + + +spring.cloud.gcp.trace.enabled +true +Auto-configure Google Cloud Stackdriver tracing components. + + +spring.cloud.gcp.trace.max-inbound-size + +Maximum size for an inbound message. + + +spring.cloud.gcp.trace.max-outbound-size + +Maximum size for an outbound message. + + +spring.cloud.gcp.trace.message-timeout + +Timeout in seconds before pending spans will be sent in batches to GCP Stackdriver Trace. + + +spring.cloud.gcp.trace.num-executor-threads +4 +Number of threads to be used by the Trace executor. + + +spring.cloud.gcp.trace.project-id + +Overrides the GCP project ID specified in the Core module. + + +spring.cloud.gcp.trace.wait-for-ready + +Waits for the channel to be ready in case of a transient failure. Defaults to failing fast in that case. + + +spring.cloud.gcp.vision.credentials.encoded-key + + + + +spring.cloud.gcp.vision.credentials.location + + + + +spring.cloud.gcp.vision.credentials.scopes + + + + +spring.cloud.gcp.vision.enabled +true +Auto-configure Google Cloud Vision components. + + +spring.cloud.httpclientfactories.apache.enabled +true +Enables creation of Apache Http Client factory beans. + + +spring.cloud.httpclientfactories.ok.enabled +true +Enables creation of OK Http Client factory beans. + + +spring.cloud.hypermedia.refresh.fixed-delay +5000 + + + +spring.cloud.hypermedia.refresh.initial-delay +10000 + + + +spring.cloud.inetutils.default-hostname +localhost +The default hostname. Used in case of errors. + + +spring.cloud.inetutils.default-ip-address +127.0.0.1 +The default IP address. Used in case of errors. + + +spring.cloud.inetutils.ignored-interfaces + +List of Java regular expressions for network interfaces that will be ignored. + + +spring.cloud.inetutils.preferred-networks + +List of Java regular expressions for network addresses that will be preferred. + + +spring.cloud.inetutils.timeout-seconds +1 +Timeout, in seconds, for calculating hostname. + + +spring.cloud.inetutils.use-only-site-local-interfaces +false +Whether to use only interfaces with site local addresses. See {@link InetAddress#isSiteLocalAddress()} for more details. + + +spring.cloud.kubernetes.client.api-version + + + + +spring.cloud.kubernetes.client.apiVersion +v1 +Kubernetes API Version + + +spring.cloud.kubernetes.client.ca-cert-data + + + + +spring.cloud.kubernetes.client.ca-cert-file + + + + +spring.cloud.kubernetes.client.caCertData + +Kubernetes API CACertData + + +spring.cloud.kubernetes.client.caCertFile + +Kubernetes API CACertFile + + +spring.cloud.kubernetes.client.client-cert-data + + + + +spring.cloud.kubernetes.client.client-cert-file + + + + +spring.cloud.kubernetes.client.client-key-algo + + + + +spring.cloud.kubernetes.client.client-key-data + + + + +spring.cloud.kubernetes.client.client-key-file + + + + +spring.cloud.kubernetes.client.client-key-passphrase + + + + +spring.cloud.kubernetes.client.clientCertData + +Kubernetes API ClientCertData + + +spring.cloud.kubernetes.client.clientCertFile + +Kubernetes API ClientCertFile + + +spring.cloud.kubernetes.client.clientKeyAlgo +RSA +Kubernetes API ClientKeyAlgo + + +spring.cloud.kubernetes.client.clientKeyData + +Kubernetes API ClientKeyData + + +spring.cloud.kubernetes.client.clientKeyFile + +Kubernetes API ClientKeyFile + + +spring.cloud.kubernetes.client.clientKeyPassphrase +changeit +Kubernetes API ClientKeyPassphrase + + +spring.cloud.kubernetes.client.connection-timeout + + + + +spring.cloud.kubernetes.client.connectionTimeout +10s +Connection timeout + + +spring.cloud.kubernetes.client.http-proxy + + + + +spring.cloud.kubernetes.client.https-proxy + + + + +spring.cloud.kubernetes.client.logging-interval + + + + +spring.cloud.kubernetes.client.loggingInterval +20s +Logging interval + + +spring.cloud.kubernetes.client.master-url + + + + +spring.cloud.kubernetes.client.masterUrl +https://kubernetes.default.svc +Kubernetes API Master Node URL + + +spring.cloud.kubernetes.client.namespace +true +Kubernetes Namespace + + +spring.cloud.kubernetes.client.no-proxy + + + + +spring.cloud.kubernetes.client.password + +Kubernetes API Password + + +spring.cloud.kubernetes.client.proxy-password + + + + +spring.cloud.kubernetes.client.proxy-username + + + + +spring.cloud.kubernetes.client.request-timeout + + + + +spring.cloud.kubernetes.client.requestTimeout +10s +Request timeout + + +spring.cloud.kubernetes.client.rolling-timeout + + + + +spring.cloud.kubernetes.client.rollingTimeout +900s +Rolling timeout + + +spring.cloud.kubernetes.client.trust-certs + + + + +spring.cloud.kubernetes.client.trustCerts +false +Kubernetes API Trust Certificates + + +spring.cloud.kubernetes.client.username + +Kubernetes API Username + + +spring.cloud.kubernetes.client.watch-reconnect-interval + + + + +spring.cloud.kubernetes.client.watch-reconnect-limit + + + + +spring.cloud.kubernetes.client.watchReconnectInterval +1s +Reconnect Interval + + +spring.cloud.kubernetes.client.watchReconnectLimit +-1 +Reconnect Interval limit retries + + +spring.cloud.kubernetes.config.enable-api +true + + + +spring.cloud.kubernetes.config.enabled +true +Enable the ConfigMap property source locator. + + +spring.cloud.kubernetes.config.name + + + + +spring.cloud.kubernetes.config.namespace + + + + +spring.cloud.kubernetes.config.paths + + + + +spring.cloud.kubernetes.config.sources + + + + +spring.cloud.kubernetes.reload.enabled +false +Enables the Kubernetes configuration reload on change. + + +spring.cloud.kubernetes.reload.mode + +Sets the detection mode for Kubernetes configuration reload. + + +spring.cloud.kubernetes.reload.monitoring-config-maps +true +Enables monitoring on config maps to detect changes. + + +spring.cloud.kubernetes.reload.monitoring-secrets +false +Enables monitoring on secrets to detect changes. + + +spring.cloud.kubernetes.reload.period +15000ms +Sets the polling period to use when the detection mode is POLLING. + + +spring.cloud.kubernetes.reload.strategy + +Sets the reload strategy for Kubernetes configuration reload on change. + + +spring.cloud.kubernetes.secrets.enable-api +false + + + +spring.cloud.kubernetes.secrets.enabled +true +Enable the Secrets property source locator. + + +spring.cloud.kubernetes.secrets.labels + + + + +spring.cloud.kubernetes.secrets.name + + + + +spring.cloud.kubernetes.secrets.namespace + + + + +spring.cloud.kubernetes.secrets.paths + + + + +spring.cloud.loadbalancer.retry.enabled +true + + + +spring.cloud.refresh.enabled +true +Enables autoconfiguration for the refresh scope and associated features. + + +spring.cloud.refresh.extra-refreshable +true +Additional class names for beans to post process into refresh scope. + + +spring.cloud.service-registry.auto-registration.enabled +true +Whether service auto-registration is enabled. Defaults to true. + + +spring.cloud.service-registry.auto-registration.fail-fast +false +Whether startup fails if there is no AutoServiceRegistration. Defaults to false. + + +spring.cloud.service-registry.auto-registration.register-management +true +Whether to register the management as a service. Defaults to true. + + +spring.cloud.stream.binders + +Additional per-binder properties (see {@link BinderProperties}) if more then one binder of the same type is used (i.e., connect to multiple instances of RabbitMq). Here you can specify multiple binder configurations, each with different environment settings. For example; spring.cloud.stream.binders.rabbit1.environment. . . , spring.cloud.stream.binders.rabbit2.environment. . . + + +spring.cloud.stream.binding-retry-interval +30 +Retry interval (in seconds) used to schedule binding attempts. Default: 30 sec. + + +spring.cloud.stream.bindings + +Additional binding properties (see {@link BinderProperties}) per binding name (e.g., 'input`). For example; This sets the content-type for the 'input' binding of a Sink application: 'spring.cloud.stream.bindings.input.contentType=text/plain' + + +spring.cloud.stream.consul.binder.event-timeout +5 + + + +spring.cloud.stream.default-binder + +The name of the binder to use by all bindings in the event multiple binders available (e.g., 'rabbit'). + + +spring.cloud.stream.dynamic-destinations +[] +A list of destinations that can be bound dynamically. If set, only listed destinations can be bound. + + +spring.cloud.stream.function.definition + +Definition of functions to bind. If several functions need to be composed into one, use pipes (e.g., 'fooFunc\|barFunc') + + +spring.cloud.stream.instance-count +1 +The number of deployed instances of an application. Default: 1. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-count" where 'foo' is the name of the binding. + + +spring.cloud.stream.instance-index +0 +The instance id of the application: a number from 0 to instanceCount-1. Used for partitioning and with Kafka. NOTE: Could also be managed per individual binding "spring.cloud.stream.bindings.foo.consumer.instance-index" where 'foo' is the name of the binding. + + +spring.cloud.stream.integration.message-handler-not-propagated-headers + +Message header names that will NOT be copied from the inbound message. + + +spring.cloud.stream.kafka.binder.auto-add-partitions +false + + + +spring.cloud.stream.kafka.binder.auto-create-topics +true + + + +spring.cloud.stream.kafka.binder.brokers +[localhost] + + + +spring.cloud.stream.kafka.binder.configuration + +Arbitrary kafka properties that apply to both producers and consumers. + + +spring.cloud.stream.kafka.binder.consumer-properties + +Arbitrary kafka consumer properties. + + +spring.cloud.stream.kafka.binder.fetch-size +0 + + + +spring.cloud.stream.kafka.binder.header-mapper-bean-name + +The bean name of a custom header mapper to use instead of a {@link org.springframework.kafka.support.DefaultKafkaHeaderMapper}. + + +spring.cloud.stream.kafka.binder.headers +[] + + + +spring.cloud.stream.kafka.binder.health-timeout +60 +Time to wait to get partition information in seconds; default 60. + + +spring.cloud.stream.kafka.binder.jaas + + + + +spring.cloud.stream.kafka.binder.max-wait +100 + + + +spring.cloud.stream.kafka.binder.min-partition-count +1 + + + +spring.cloud.stream.kafka.binder.offset-update-count +0 + + + +spring.cloud.stream.kafka.binder.offset-update-shutdown-timeout +2000 + + + +spring.cloud.stream.kafka.binder.offset-update-time-window +10000 + + + +spring.cloud.stream.kafka.binder.producer-properties + +Arbitrary kafka producer properties. + + +spring.cloud.stream.kafka.binder.queue-size +8192 + + + +spring.cloud.stream.kafka.binder.replication-factor +1 + + + +spring.cloud.stream.kafka.binder.required-acks +1 + + + +spring.cloud.stream.kafka.binder.socket-buffer-size +2097152 + + + +spring.cloud.stream.kafka.binder.transaction.producer.admin + + + + +spring.cloud.stream.kafka.binder.transaction.producer.batch-timeout + + + + +spring.cloud.stream.kafka.binder.transaction.producer.buffer-size + + + + +spring.cloud.stream.kafka.binder.transaction.producer.compression-type + + + + +spring.cloud.stream.kafka.binder.transaction.producer.configuration + + + + +spring.cloud.stream.kafka.binder.transaction.producer.error-channel-enabled + + + + +spring.cloud.stream.kafka.binder.transaction.producer.header-mode + + + + +spring.cloud.stream.kafka.binder.transaction.producer.header-patterns + + + + +spring.cloud.stream.kafka.binder.transaction.producer.message-key-expression + + + + +spring.cloud.stream.kafka.binder.transaction.producer.partition-count + + + + +spring.cloud.stream.kafka.binder.transaction.producer.partition-key-expression + + + + +spring.cloud.stream.kafka.binder.transaction.producer.partition-key-extractor-name + + + + +spring.cloud.stream.kafka.binder.transaction.producer.partition-selector-expression + + + + +spring.cloud.stream.kafka.binder.transaction.producer.partition-selector-name + + + + +spring.cloud.stream.kafka.binder.transaction.producer.required-groups + + + + +spring.cloud.stream.kafka.binder.transaction.producer.sync + + + + +spring.cloud.stream.kafka.binder.transaction.producer.topic + + + + +spring.cloud.stream.kafka.binder.transaction.producer.use-native-encoding + + + + +spring.cloud.stream.kafka.binder.transaction.transaction-id-prefix + + + + +spring.cloud.stream.kafka.binder.zk-connection-timeout +10000 +ZK Connection timeout in milliseconds. + + +spring.cloud.stream.kafka.binder.zk-nodes +[localhost] + + + +spring.cloud.stream.kafka.binder.zk-session-timeout +10000 +ZK session timeout in milliseconds. + + +spring.cloud.stream.kafka.bindings + + + + +spring.cloud.stream.kafka.streams.binder.application-id + + + + +spring.cloud.stream.kafka.streams.binder.auto-add-partitions + + + + +spring.cloud.stream.kafka.streams.binder.auto-create-topics + + + + +spring.cloud.stream.kafka.streams.binder.brokers + + + + +spring.cloud.stream.kafka.streams.binder.configuration + + + + +spring.cloud.stream.kafka.streams.binder.consumer-properties + + + + +spring.cloud.stream.kafka.streams.binder.fetch-size + + + + +spring.cloud.stream.kafka.streams.binder.header-mapper-bean-name + + + + +spring.cloud.stream.kafka.streams.binder.headers + + + + +spring.cloud.stream.kafka.streams.binder.health-timeout + + + + +spring.cloud.stream.kafka.streams.binder.jaas + + + + +spring.cloud.stream.kafka.streams.binder.max-wait + + + + +spring.cloud.stream.kafka.streams.binder.min-partition-count + + + + +spring.cloud.stream.kafka.streams.binder.offset-update-count + + + + +spring.cloud.stream.kafka.streams.binder.offset-update-shutdown-timeout + + + + +spring.cloud.stream.kafka.streams.binder.offset-update-time-window + + + + +spring.cloud.stream.kafka.streams.binder.producer-properties + + + + +spring.cloud.stream.kafka.streams.binder.queue-size + + + + +spring.cloud.stream.kafka.streams.binder.replication-factor + + + + +spring.cloud.stream.kafka.streams.binder.required-acks + + + + +spring.cloud.stream.kafka.streams.binder.serde-error + +{@link org.apache.kafka.streams.errors.DeserializationExceptionHandler} to use when there is a Serde error. {@link KafkaStreamsBinderConfigurationProperties.SerdeError} values are used to provide the exception handler on consumer binding. + + +spring.cloud.stream.kafka.streams.binder.socket-buffer-size + + + + +spring.cloud.stream.kafka.streams.binder.zk-connection-timeout + + + + +spring.cloud.stream.kafka.streams.binder.zk-nodes + + + + +spring.cloud.stream.kafka.streams.binder.zk-session-timeout + + + + +spring.cloud.stream.kafka.streams.bindings + + + + +spring.cloud.stream.kafka.streams.time-window.advance-by +0 + + + +spring.cloud.stream.kafka.streams.time-window.length +0 + + + +spring.cloud.stream.metrics.export-properties + +List of properties that are going to be appended to each message. This gets populate by onApplicationEvent, once the context refreshes to avoid overhead of doing per message basis. + + +spring.cloud.stream.metrics.key + +The name of the metric being emitted. Should be an unique value per application. Defaults to: ${spring.application.name:${vcap.application.name:${spring.config.name:application}}}. + + +spring.cloud.stream.metrics.meter-filter + +Pattern to control the 'meters' one wants to capture. By default all 'meters' will be captured. For example, 'spring.integration.*' will only capture metric information for meters whose name starts with 'spring.integration'. + + +spring.cloud.stream.metrics.properties + +Application properties that should be added to the metrics payload For example: spring.application**. + + +spring.cloud.stream.metrics.schedule-interval +60s +Interval expressed as Duration for scheduling metrics snapshots publishing. Defaults to 60 seconds + + +spring.cloud.stream.override-cloud-connectors +false +This property is only applicable when the cloud profile is active and Spring Cloud Connectors are provided with the application. If the property is false (the default), the binder detects a suitable bound service (for example, a RabbitMQ service bound in Cloud Foundry for the RabbitMQ binder) and uses it for creating connections (usually through Spring Cloud Connectors). When set to true, this property instructs binders to completely ignore the bound services and rely on Spring Boot properties (for example, relying on the spring.rabbitmq.* properties provided in the environment for the RabbitMQ binder). The typical usage of this property is to be nested in a customized environment when connecting to multiple systems. + + +spring.cloud.stream.rabbit.binder.admin-addresses +[] +Urls for management plugins; only needed for queue affinity. + + +spring.cloud.stream.rabbit.binder.admin-adresses + + + + +spring.cloud.stream.rabbit.binder.compression-level +0 +Compression level for compressed bindings; see 'java.util.zip.Deflator'. + + +spring.cloud.stream.rabbit.binder.connection-name-prefix + +Prefix for connection names from this binder. + + +spring.cloud.stream.rabbit.binder.nodes +[] +Cluster member node names; only needed for queue affinity. + + +spring.cloud.stream.rabbit.bindings + + + + +spring.cloud.stream.schema-registry-client.cached +false + + + +spring.cloud.stream.schema-registry-client.endpoint + + + + +spring.cloud.stream.schema.avro.dynamic-schema-generation-enabled +false + + + +spring.cloud.stream.schema.avro.prefix +vnd + + + +spring.cloud.stream.schema.avro.reader-schema + + + + +spring.cloud.stream.schema.avro.schema-imports + +A list of files or directories that should be loaded first thus making them importable by subsequent schemas. Note that imported files should not reference each other. @parameter + + +spring.cloud.stream.schema.avro.schema-locations + +The source directory of Apache Avro schema. This schema is used by this converter. If this schema depends on other schemas consider defining those those dependent ones in the {@link #schemaImports} @parameter + + +spring.cloud.stream.schema.server.allow-schema-deletion +false +Boolean flag to enable/disable schema deletion. + + +spring.cloud.stream.schema.server.path + +Prefix for configuration resource paths (default is empty). Useful when embedding in another application when you don’t want to change the context path or servlet path. + + +spring.cloud.task.batch.command-line-runner-order +0 +The order for the {@code CommandLineRunner} used to run batch jobs when {@code spring.cloud.task.batch.fail-on-job-failure=true}. Defaults to 0 (same as the {@link org.springframework.boot.autoconfigure.batch.JobLauncherCommandLineRunner}). + + +spring.cloud.task.batch.events.chunk-order + +Properties for chunk listener order + + +spring.cloud.task.batch.events.chunk.enabled +true +This property is used to determine if a task should listen for batch chunk events. + + +spring.cloud.task.batch.events.enabled +true +This property is used to determine if a task should listen for batch events. + + +spring.cloud.task.batch.events.item-process-order + +Properties for itemProcess listener order + + +spring.cloud.task.batch.events.item-process.enabled +true +This property is used to determine if a task should listen for batch item processed events. + + +spring.cloud.task.batch.events.item-read-order + +Properties for itemRead listener order + + +spring.cloud.task.batch.events.item-read.enabled +true +This property is used to determine if a task should listen for batch item read events. + + +spring.cloud.task.batch.events.item-write-order + +Properties for itemWrite listener order + + +spring.cloud.task.batch.events.item-write.enabled +true +This property is used to determine if a task should listen for batch item write events. + + +spring.cloud.task.batch.events.job-execution-order + +Properties for jobExecution listener order + + +spring.cloud.task.batch.events.job-execution.enabled +true +This property is used to determine if a task should listen for batch job execution events. + + +spring.cloud.task.batch.events.skip-order + +Properties for skip listener order + + +spring.cloud.task.batch.events.skip.enabled +true +This property is used to determine if a task should listen for batch skip events. + + +spring.cloud.task.batch.events.step-execution-order + +Properties for stepExecution listener order + + +spring.cloud.task.batch.events.step-execution.enabled +true +This property is used to determine if a task should listen for batch step execution events. + + +spring.cloud.task.batch.fail-on-job-failure +false +This property is used to determine if a task app should return with a non zero exit code if a batch job fails. + + +spring.cloud.task.batch.fail-on-job-failure-poll-interval +5000 +Fixed delay in milliseconds that Spring Cloud Task will wait when checking if {@link org.springframework.batch.core.JobExecution}s have completed, when spring.cloud.task.batch.failOnJobFailure is set to true. Defaults to 5000. + + +spring.cloud.task.batch.job-names + +Comma-separated list of job names to execute on startup (for instance, job1,job2). By default, all Jobs found in the context are executed. + + +spring.cloud.task.batch.listener.enabled +true +This property is used to determine if a task will be linked to the batch jobs that are run. + + +spring.cloud.task.closecontext-enabled +false +When set to true the context is closed at the end of the task. Else the context remains open. + + +spring.cloud.task.events.enabled +true +This property is used to determine if a task app should emit task events. + + +spring.cloud.task.executionid + +An id that will be used by the task when updating the task execution. + + +spring.cloud.task.external-execution-id + +An id that can be associated with a task. + + +spring.cloud.task.parent-execution-id + +The id of the parent task execution id that launched this task execution. Defaults to null if task execution had no parent. + + +spring.cloud.task.single-instance-enabled +false +This property is used to determine if a task will execute if another task with the same app name is running. + + +spring.cloud.task.single-instance-lock-check-interval +500 +Declares the time (in millis) that a task execution will wait between checks. Default time is: 500 millis. + + +spring.cloud.task.single-instance-lock-ttl + +Declares the maximum amount of time (in millis) that a task execution can hold a lock to prevent another task from executing with a specific task name when the single-instance-enabled is set to true. Default time is: Integer.MAX_VALUE. + + +spring.cloud.task.table-prefix +TASK_ +The prefix to append to the table names created by Spring Cloud Task. + + +spring.cloud.util.enabled +true +Enables creation of Spring Cloud utility beans. + + +spring.cloud.vault.app-id.app-id-path +app-id +Mount path of the AppId authentication backend. + + +spring.cloud.vault.app-id.network-interface + +Network interface hint for the "MAC_ADDRESS" UserId mechanism. + + +spring.cloud.vault.app-id.user-id +MAC_ADDRESS +UserId mechanism. Can be either "MAC_ADDRESS", "IP_ADDRESS", a string or a class name. + + +spring.cloud.vault.app-role.app-role-path +approle +Mount path of the AppRole authentication backend. + + +spring.cloud.vault.app-role.role + +Name of the role, optional, used for pull-mode. + + +spring.cloud.vault.app-role.role-id + +The RoleId. + + +spring.cloud.vault.app-role.secret-id + +The SecretId. + + +spring.cloud.vault.application-name +application +Application name for AppId authentication. + + +spring.cloud.vault.authentication + + + + +spring.cloud.vault.aws-ec2.aws-ec2-path +aws-ec2 +Mount path of the AWS-EC2 authentication backend. + + +spring.cloud.vault.aws-ec2.identity-document +http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 +URL of the AWS-EC2 PKCS7 identity document. + + +spring.cloud.vault.aws-ec2.nonce + +Nonce used for AWS-EC2 authentication. An empty nonce defaults to nonce generation. + + +spring.cloud.vault.aws-ec2.role + +Name of the role, optional. + + +spring.cloud.vault.aws-iam.aws-path +aws +Mount path of the AWS authentication backend. + + +spring.cloud.vault.aws-iam.role + +Name of the role, optional. Defaults to the friendly IAM name if not set. + + +spring.cloud.vault.aws-iam.server-name + +Name of the server used to set {@code X-Vault-AWS-IAM-Server-ID} header in the headers of login requests. + + +spring.cloud.vault.aws.access-key-property +cloud.aws.credentials.accessKey +Target property for the obtained access key. + + +spring.cloud.vault.aws.backend +aws +aws backend path. + + +spring.cloud.vault.aws.enabled +false +Enable aws backend usage. + + +spring.cloud.vault.aws.role + +Role name for credentials. + + +spring.cloud.vault.aws.secret-key-property +cloud.aws.credentials.secretKey +Target property for the obtained secret key. + + +spring.cloud.vault.azure-msi.azure-path +azure +Mount path of the Azure MSI authentication backend. + + +spring.cloud.vault.azure-msi.role + +Name of the role. + + +spring.cloud.vault.cassandra.backend +cassandra +Cassandra backend path. + + +spring.cloud.vault.cassandra.enabled +false +Enable cassandra backend usage. + + +spring.cloud.vault.cassandra.password-property +spring.data.cassandra.password +Target property for the obtained password. + + +spring.cloud.vault.cassandra.role + +Role name for credentials. + + +spring.cloud.vault.cassandra.username-property +spring.data.cassandra.username +Target property for the obtained username. + + +spring.cloud.vault.config.lifecycle.enabled +true +Enable lifecycle management. + + +spring.cloud.vault.config.order +0 +Used to set a {@link org.springframework.core.env.PropertySource} priority. This is useful to use Vault as an override on other property sources. @see org.springframework.core.PriorityOrdered + + +spring.cloud.vault.connection-timeout +5000 +Connection timeout. + + +spring.cloud.vault.consul.backend +consul +Consul backend path. + + +spring.cloud.vault.consul.enabled +false +Enable consul backend usage. + + +spring.cloud.vault.consul.role + +Role name for credentials. + + +spring.cloud.vault.consul.token-property +spring.cloud.consul.token +Target property for the obtained token. + + +spring.cloud.vault.database.backend +database +Database backend path. + + +spring.cloud.vault.database.enabled +false +Enable database backend usage. + + +spring.cloud.vault.database.password-property +spring.datasource.password +Target property for the obtained password. + + +spring.cloud.vault.database.role + +Role name for credentials. + + +spring.cloud.vault.database.username-property +spring.datasource.username +Target property for the obtained username. + + +spring.cloud.vault.discovery.enabled +false +Flag to indicate that Vault server discovery is enabled (vault server URL will be looked up via discovery). + + +spring.cloud.vault.discovery.service-id +vault +Service id to locate Vault. + + +spring.cloud.vault.enabled +true +Enable Vault config server. + + +spring.cloud.vault.fail-fast +false +Fail fast if data cannot be obtained from Vault. + + +spring.cloud.vault.gcp-gce.gcp-path +gcp +Mount path of the Kubernetes authentication backend. + + +spring.cloud.vault.gcp-gce.role + +Name of the role against which the login is being attempted. + + +spring.cloud.vault.gcp-gce.service-account + +Optional service account id. Using the default id if left unconfigured. + + +spring.cloud.vault.gcp-iam.credentials.encoded-key + +The base64 encoded contents of an OAuth2 account private key in JSON format. + + +spring.cloud.vault.gcp-iam.credentials.location + +Location of the OAuth2 credentials private key. <p> Since this is a Resource, the private key can be in a multitude of locations, such as a local file system, classpath, URL, etc. + + +spring.cloud.vault.gcp-iam.gcp-path +gcp +Mount path of the Kubernetes authentication backend. + + +spring.cloud.vault.gcp-iam.jwt-validity +15m +Validity of the JWT token. + + +spring.cloud.vault.gcp-iam.project-id + +Overrides the GCP project Id. + + +spring.cloud.vault.gcp-iam.role + +Name of the role against which the login is being attempted. + + +spring.cloud.vault.gcp-iam.service-account-id + +Overrides the GCP service account Id. + + +spring.cloud.vault.generic.application-name +application +Application name to be used for the context. + + +spring.cloud.vault.generic.backend +secret +Name of the default backend. + + +spring.cloud.vault.generic.default-context +application +Name of the default context. + + +spring.cloud.vault.generic.enabled +true +Enable the generic backend. + + +spring.cloud.vault.generic.profile-separator +/ +Profile-separator to combine application name and profile. + + +spring.cloud.vault.host +localhost +Vault server host. + + +spring.cloud.vault.kubernetes.kubernetes-path +kubernetes +Mount path of the Kubernetes authentication backend. + + +spring.cloud.vault.kubernetes.role + +Name of the role against which the login is being attempted. + + +spring.cloud.vault.kubernetes.service-account-token-file +/var/run/secrets/kubernetes.io/serviceaccount/token +Path to the service account token file. + + +spring.cloud.vault.kv.application-name +application +Application name to be used for the context. + + +spring.cloud.vault.kv.backend +secret +Name of the default backend. + + +spring.cloud.vault.kv.backend-version +2 +Key-Value backend version. Currently supported versions are: <ul> <li>Version 1 (unversioned key-value backend).</li> <li>Version 2 (versioned key-value backend).</li> </ul> + + +spring.cloud.vault.kv.default-context +application +Name of the default context. + + +spring.cloud.vault.kv.enabled +false +Enable the kev-value backend. + + +spring.cloud.vault.kv.profile-separator +/ +Profile-separator to combine application name and profile. + + +spring.cloud.vault.mongodb.backend +mongodb +Cassandra backend path. + + +spring.cloud.vault.mongodb.enabled +false +Enable mongodb backend usage. + + +spring.cloud.vault.mongodb.password-property +spring.data.mongodb.password +Target property for the obtained password. + + +spring.cloud.vault.mongodb.role + +Role name for credentials. + + +spring.cloud.vault.mongodb.username-property +spring.data.mongodb.username +Target property for the obtained username. + + +spring.cloud.vault.mysql.backend +mysql +mysql backend path. + + +spring.cloud.vault.mysql.enabled +false +Enable mysql backend usage. + + +spring.cloud.vault.mysql.password-property +spring.datasource.password +Target property for the obtained username. + + +spring.cloud.vault.mysql.role + +Role name for credentials. + + +spring.cloud.vault.mysql.username-property +spring.datasource.username +Target property for the obtained username. + + +spring.cloud.vault.port +8200 +Vault server port. + + +spring.cloud.vault.postgresql.backend +postgresql +postgresql backend path. + + +spring.cloud.vault.postgresql.enabled +false +Enable postgresql backend usage. + + +spring.cloud.vault.postgresql.password-property +spring.datasource.password +Target property for the obtained username. + + +spring.cloud.vault.postgresql.role + +Role name for credentials. + + +spring.cloud.vault.postgresql.username-property +spring.datasource.username +Target property for the obtained username. + + +spring.cloud.vault.rabbitmq.backend +rabbitmq +rabbitmq backend path. + + +spring.cloud.vault.rabbitmq.enabled +false +Enable rabbitmq backend usage. + + +spring.cloud.vault.rabbitmq.password-property +spring.rabbitmq.password +Target property for the obtained password. + + +spring.cloud.vault.rabbitmq.role + +Role name for credentials. + + +spring.cloud.vault.rabbitmq.username-property +spring.rabbitmq.username +Target property for the obtained username. + + +spring.cloud.vault.read-timeout +15000 +Read timeout. + + +spring.cloud.vault.scheme +https +Protocol scheme. Can be either "http" or "https". + + +spring.cloud.vault.ssl.cert-auth-path +cert +Mount path of the TLS cert authentication backend. + + +spring.cloud.vault.ssl.key-store + +Trust store that holds certificates and private keys. + + +spring.cloud.vault.ssl.key-store-password + +Password used to access the key store. + + +spring.cloud.vault.ssl.trust-store + +Trust store that holds SSL certificates. + + +spring.cloud.vault.ssl.trust-store-password + +Password used to access the trust store. + + +spring.cloud.vault.token + +Static vault token. Required if {@link #authentication} is {@code TOKEN}. + + +spring.cloud.vault.uri + +Vault URI. Can be set with scheme, host and port. + + +spring.cloud.zookeeper.base-sleep-time-ms +50 +Initial amount of time to wait between retries. + + +spring.cloud.zookeeper.block-until-connected-unit + +The unit of time related to blocking on connection to Zookeeper. + + +spring.cloud.zookeeper.block-until-connected-wait +10 +Wait time to block on connection to Zookeeper. + + +spring.cloud.zookeeper.connect-string +localhost:2181 +Connection string to the Zookeeper cluster. + + +spring.cloud.zookeeper.default-health-endpoint + +Default health endpoint that will be checked to verify that a dependency is alive. + + +spring.cloud.zookeeper.dependencies + +Mapping of alias to ZookeeperDependency. From Ribbon perspective the alias is actually serviceID since Ribbon can’t accept nested structures in serviceID. + + +spring.cloud.zookeeper.dependency-configurations + + + + +spring.cloud.zookeeper.dependency-names + + + + +spring.cloud.zookeeper.discovery.enabled +true + + + +spring.cloud.zookeeper.discovery.initial-status + +The initial status of this instance (defaults to {@link StatusConstants#STATUS_UP}). + + +spring.cloud.zookeeper.discovery.instance-host + +Predefined host with which a service can register itself in Zookeeper. Corresponds to the {code address} from the URI spec. + + +spring.cloud.zookeeper.discovery.instance-id + +Id used to register with zookeeper. Defaults to a random UUID. + + +spring.cloud.zookeeper.discovery.instance-port + +Port to register the service under (defaults to listening port). + + +spring.cloud.zookeeper.discovery.instance-ssl-port + +Ssl port of the registered service. + + +spring.cloud.zookeeper.discovery.metadata + +Gets the metadata name/value pairs associated with this instance. This information is sent to zookeeper and can be used by other instances. + + +spring.cloud.zookeeper.discovery.order +0 +Order of the discovery client used by CompositeDiscoveryClient for sorting available clients. + + +spring.cloud.zookeeper.discovery.register +true +Register as a service in zookeeper. + + +spring.cloud.zookeeper.discovery.root +/services +Root Zookeeper folder in which all instances are registered. + + +spring.cloud.zookeeper.discovery.uri-spec +{scheme}://{address}:{port} +The URI specification to resolve during service registration in Zookeeper. + + +spring.cloud.zookeeper.enabled +true +Is Zookeeper enabled. + + +spring.cloud.zookeeper.max-retries +10 +Max number of times to retry. + + +spring.cloud.zookeeper.max-sleep-ms +500 +Max time in ms to sleep on each retry. + + +spring.cloud.zookeeper.prefix + +Common prefix that will be applied to all Zookeeper dependencies' paths. + + +spring.integration.poller.fixed-delay +1000 +Fixed delay for default poller. + + +spring.integration.poller.max-messages-per-poll +1 +Maximum messages per poll for the default poller. + + +spring.sleuth.annotation.enabled +true + + + +spring.sleuth.async.configurer.enabled +true +Enable default AsyncConfigurer. + + +spring.sleuth.async.enabled +true +Enable instrumenting async related components so that the tracing information is passed between threads. + + +spring.sleuth.async.ignored-beans + +List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation. + + +spring.sleuth.baggage-keys + +List of baggage key names that should be propagated out of process. These keys will be prefixed with baggage before the actual key. This property is set in order to be backward compatible with previous Sleuth versions. @see brave.propagation.ExtraFieldPropagation.FactoryBuilder#addPrefixedFields(String, java.util.Collection) + + +spring.sleuth.enabled +true + + + +spring.sleuth.feign.enabled +true +Enable span information propagation when using Feign. + + +spring.sleuth.feign.processor.enabled +true +Enable post processor that wraps Feign Context in its tracing representations. + + +spring.sleuth.grpc.enabled +true +Enable span information propagation when using GRPC. + + +spring.sleuth.http.enabled +true + + + +spring.sleuth.http.legacy.enabled +false +Enables the legacy Sleuth setup. + + +spring.sleuth.hystrix.strategy.enabled +true +Enable custom HystrixConcurrencyStrategy that wraps all Callable instances into their Sleuth representative - the TraceCallable. + + +spring.sleuth.integration.enabled +true +Enable Spring Integration sleuth instrumentation. + + +spring.sleuth.integration.patterns +[!hystrixStreamOutput*, *] +An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream channel name. + + +spring.sleuth.integration.websockets.enabled +true +Enable tracing for WebSockets. + + +spring.sleuth.keys.http.headers + +Additional headers that should be added as tags if they exist. If the header value is multi-valued, the tag value will be a comma-separated, single-quoted list. + + +spring.sleuth.keys.http.prefix +http. +Prefix for header names if they are added as tags. + + +spring.sleuth.log.slf4j.enabled +true +Enable a {@link Slf4jScopeDecorator} that prints tracing information in the logs. + + +spring.sleuth.log.slf4j.whitelisted-mdc-keys + +A list of keys to be put from baggage to MDC. + + +spring.sleuth.messaging.enabled +false +Should messaging be turned on. + + +spring.sleuth.messaging.jms.enabled +false + + + +spring.sleuth.messaging.jms.remote-service-name +jms + + + +spring.sleuth.messaging.kafka.enabled +false + + + +spring.sleuth.messaging.kafka.remote-service-name +kafka + + + +spring.sleuth.messaging.rabbit.enabled +false + + + +spring.sleuth.messaging.rabbit.remote-service-name +rabbitmq + + + +spring.sleuth.opentracing.enabled +true + + + +spring.sleuth.propagation-keys + +List of fields that are referenced the same in-process as it is on the wire. For example, the name "x-vcap-request-id" would be set as-is including the prefix. <p> Note: {@code fieldName} will be implicitly lower-cased. @see brave.propagation.ExtraFieldPropagation.FactoryBuilder#addField(String) + + +spring.sleuth.propagation.tag.enabled +true +Enables a {@link TagPropagationFinishedSpanHandler} that adds extra propagated fields to span tags. + + +spring.sleuth.propagation.tag.whitelisted-keys + +A list of keys to be put from extra propagation fields to span tags. + + +spring.sleuth.reactor.decorate-on-each +true +When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries. + + +spring.sleuth.reactor.enabled +true +When true enables instrumentation for reactor. + + +spring.sleuth.rxjava.schedulers.hook.enabled +true +Enable support for RxJava via RxJavaSchedulersHook. + + +spring.sleuth.rxjava.schedulers.ignoredthreads +[HystrixMetricPoller, ^RxComputation.*$] +Thread names for which spans will not be sampled. + + +spring.sleuth.sampler.probability +0.1 +Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there’s no support for 0.1% of the traces). + + +spring.sleuth.sampler.rate + +A rate per second can be a nice choice for low-traffic endpoints as it allows you surge protection. For example, you may never expect the endpoint to get more than 50 requests per second. If there was a sudden surge of traffic, to 5000 requests per second, you would still end up with 50 traces per second. Conversely, if you had a percentage, like 10%, the same surge would end up with 500 traces per second, possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler (named Reservoir) for this purpose. Brave has taken the same approach via the {@link brave.sampler.RateLimitingSampler}. + + +spring.sleuth.scheduled.enabled +true +Enable tracing for {@link org.springframework.scheduling.annotation.Scheduled}. + + +spring.sleuth.scheduled.skip-pattern +org.springframework.cloud.netflix.hystrix.stream.HystrixStreamTask +Pattern for the fully qualified name of a class that should be skipped. + + +spring.sleuth.supports-join +true +True means the tracing system supports sharing a span ID between a client and server. + + +spring.sleuth.trace-id128 +false +When true, generate 128-bit trace IDs instead of 64-bit ones. + + +spring.sleuth.web.additional-skip-pattern + +Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. + + +spring.sleuth.web.client.enabled +true +Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}. + + +spring.sleuth.web.client.skip-pattern + +Pattern for URLs that should be skipped in client side tracing. + + +spring.sleuth.web.enabled +true +When true enables instrumentation for web applications. + + +spring.sleuth.web.exception-logging-filter-enabled +true +Flag to toggle the presence of a filter that logs thrown exceptions. + + +spring.sleuth.web.exception-throwing-filter-enabled +true +Flag to toggle the presence of a filter that logs thrown exceptions. @deprecated use {@link #exceptionLoggingFilterEnabled} + + +spring.sleuth.web.filter-order + +Order in which the tracing filters should be registered. Defaults to {@link TraceHttpAutoConfiguration#TRACING_FILTER_ORDER}. + + +spring.sleuth.web.ignore-auto-configured-skip-patterns +false +If set to true, auto-configured skip patterns will be ignored. @see TraceWebAutoConfiguration + + +spring.sleuth.web.skip-pattern +/api-docs.|/swagger.|.\.png|.\.css|.\.js|.\.html|/favicon.ico|/hystrix.stream +Pattern for URLs that should be skipped in tracing. + + +spring.sleuth.zuul.enabled +true +Enable span information propagation when using Zuul. + + +spring.zipkin.base-url +http://localhost:9411/ +URL of the zipkin query server instance. You can also provide the service id of the Zipkin server if Zipkin’s registered in service discovery (e.g. http://zipkinserver/). + + +spring.zipkin.compression.enabled +false + + + +spring.zipkin.discovery-client-enabled + +If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL always. + + +spring.zipkin.enabled +true +Enables sending spans to Zipkin. + + +spring.zipkin.encoder + +Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server is not recent. + + +spring.zipkin.locator.discovery.enabled +false +Enabling of locating the host name via service discovery. + + +spring.zipkin.message-timeout +1 +Timeout in seconds before pending spans will be sent in batches to Zipkin. + + +spring.zipkin.sender.type + +Means of sending spans to Zipkin. + + +spring.zipkin.service.name + +The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin. + + +stubrunner.amqp.enabled +false +Whether to enable support for Stub Runner and AMQP. + + +stubrunner.amqp.mockCOnnection +true +Whether to enable support for Stub Runner and AMQP mocked connection factory. + + +stubrunner.classifier +stubs +The classifier to use by default in ivy co-ordinates for a stub. + + +stubrunner.cloud.consul.enabled +true +Whether to enable stubs registration in Consul. + + +stubrunner.cloud.delegate.enabled +true +Whether to enable DiscoveryClient’s Stub Runner implementation. + + +stubrunner.cloud.enabled +true +Whether to enable Spring Cloud support for Stub Runner. + + +stubrunner.cloud.eureka.enabled +true +Whether to enable stubs registration in Eureka. + + +stubrunner.cloud.ribbon.enabled +true +Whether to enable Stub Runner’s Ribbon integration. + + +stubrunner.cloud.stubbed.discovery.enabled +true +Whether Service Discovery should be stubbed for Stub Runner. If set to false, stubs will get registered in real service discovery. + + +stubrunner.cloud.zookeeper.enabled +true +Whether to enable stubs registration in Zookeeper. + + +stubrunner.consumer-name + +You can override the default {@code spring.application.name} of this field by setting a value to this parameter. + + +stubrunner.delete-stubs-after-test +true +If set to {@code false} will NOT delete stubs from a temporary folder after running tests. + + +stubrunner.http-server-stub-configurer + +Configuration for an HTTP server stub. + + +stubrunner.ids +[] +The ids of the stubs to run in "ivy" notation ([groupId]:artifactId:[version]:[classifier][:port]). {@code groupId}, {@code classifier}, {@code version} and {@code port} can be optional. + + +stubrunner.ids-to-service-ids + +Mapping of Ivy notation based ids to serviceIds inside your application. Example "a:b" → "myService" "artifactId" → "myOtherService" + + +stubrunner.integration.enabled +true +Whether to enable Stub Runner integration with Spring Integration. + + +stubrunner.mappings-output-folder + +Dumps the mappings of each HTTP server to the selected folder. + + +stubrunner.max-port +15000 +Max value of a port for the automatically started WireMock server. + + +stubrunner.min-port +10000 +Min value of a port for the automatically started WireMock server. + + +stubrunner.password + +Repository password. + + +stubrunner.properties + +Map of properties that can be passed to custom {@link org.springframework.cloud.contract.stubrunner.StubDownloaderBuilder}. + + +stubrunner.proxy-host + +Repository proxy host. + + +stubrunner.proxy-port + +Repository proxy port. + + +stubrunner.stream.enabled +true +Whether to enable Stub Runner integration with Spring Cloud Stream. + + +stubrunner.stubs-mode + +Pick where the stubs should come from. + + +stubrunner.stubs-per-consumer +false +Should only stubs for this particular consumer get registered in HTTP server stub. + + +stubrunner.username + +Repository username. + + +wiremock.rest-template-ssl-enabled +false + + + +wiremock.server.files +[] + + + +wiremock.server.https-port +-1 + + + +wiremock.server.https-port-dynamic +false + + + +wiremock.server.port +8080 + + + +wiremock.server.port-dynamic +false + + + +wiremock.server.stubs +[] + + + + + + + +
    \ No newline at end of file