كان لدينا JDK 11 و Kotlin و Spring 5 و Spring Boot 2 و Gradle 5 مع Kotlin DSL الجاهزة للإنتاج و JUnit 5 وعشرات مكتبات تكديس Spring Cloud لاكتشاف الخدمة وإنشاء واجهات برمجة التطبيقات الخاصة بالبوابة وموازنة العميل وتنفيذ قواطع دوائر كتابة عملاء HTTP التعريفي والتتبع الموزع وكل ذلك. ليس هذا كل ما هو مطلوب لإنشاء بنية microservice - فقط للمتعة فقط ...الدخول
في هذه المقالة ، سترى مثالًا على بنية الخدمات الميكروية باستخدام التقنيات ذات الصلة في عالم Java ، والتي يتم تقديم أهمها أدناه (يتم استخدام الإصدارات المشار إليها في المشروع في وقت النشر):
يتكون المشروع من 5 خدمات مصغرة: 3 بنية أساسية (خادم التهيئة ، خادم اكتشاف الخدمة ، بوابة واجهة المستخدم) وأمثلة من الواجهة الأمامية (العناصر واجهة المستخدم) والجهة الخلفية (خدمة العناصر):
كل منهم سوف ينظر بالتتابع أدناه. في مشروع "قتالي" ، من الواضح ، سيكون هناك المزيد من الخدمات المصغرة التي تنفذ أي وظيفة تجارية. تتم إضافتها إلى بنية مشابهة تقنيًا بالطريقة نفسها مثل خدمة عناصر واجهة المستخدم والعناصر.
إخلاء المسؤولية
لا تعتبر المقالة أدوات لتعبئة الحاويات وتنسيقها ، حيث أنها لا تُستخدم في الوقت الحالي في المشروع.
خادم التكوين
تم استخدام Spring Cloud Config لإنشاء مستودع مركزي لتكوينات التطبيق. يمكن قراءة Configs من مصادر مختلفة ، على سبيل المثال ، مستودع git منفصل ؛ في هذا المشروع ، للبساطة والوضوح ، فهي في موارد التطبيق:
في هذه الحالة ، يبدو تكوين خادم التكوين (
application.yml
) نفسه كما يلي:
spring: profiles: active: native cloud: config: server: native: search-locations: classpath:/config server: port: 8888
باستخدام المنفذ 8888 يسمح لعملاء ملقم التكوين بعدم تحديد منفذه بشكل صريح في
bootstrap.yml
. عند بدء التشغيل ، يقومون بتحميل التكوين الخاص بهم عن طريق تنفيذ طلب GET على خادم HTTP API Config.
يتكون رمز البرنامج لهذه الخدمة المجهرية من ملف واحد فقط ، يحتوي على إعلان فئة التطبيق والطريقة الرئيسية ، والتي ، على عكس كود جافا المكافئ ، هي وظيفة من المستوى الأعلى:
@SpringBootApplication @EnableConfigServer class ConfigServerApplication fun main(args: Array<String>) { runApplication<ConfigServerApplication>(*args) }
فئات التطبيق والأساليب الرئيسية في الخدمات الصغيرة الأخرى لها مظهر مماثل.
خادم اكتشاف الخدمة
اكتشاف الخدمة هو نمط هندسة الخدمة المصغرة الذي يتيح لك تبسيط التفاعل بين التطبيقات في مواجهة تغيير محتمل في عدد مثيلاتها وموقع الشبكة. أحد المكونات الرئيسية في هذا النهج هو سجل الخدمة - قاعدة بيانات عن الخدمات المصغرة ومثيلاتها ومواقع الشبكة (مزيد من التفاصيل
هنا ).
في هذا المشروع ، يتم تطبيق اكتشاف الخدمة على أساس Netflix Eureka ، وهو
اكتشاف للخدمة من جانب العميل : يقوم خادم Eureka بوظيفة سجل الخدمة ، وعميل Eureka ، قبل تنفيذ طلب إلى أي خدمة microservices ، اتصل بخادم Eureka للحصول على قائمة بمثيلات التطبيق الذي تم استدعاؤه ، تحميل (باستخدام Netflix Ribbon). يتكامل Netflix Eureka ، مثل بعض مكونات مكدس OSS الأخرى من Netflix (مثل Hystrix و Ribbon) مع تطبيقات Spring Boot باستخدام
Spring Cloud Netflix .
في تكوين خادم اكتشاف الخدمة الموجود في موارده (
bootstrap.yml
) ، يتم الإشارة فقط إلى اسم التطبيق والمعلمة التي تشير إلى أن بداية الخدمة المصغرة ستتم مقاطعة إذا كان من المستحيل الاتصال بخادم التكوين:
spring: application: name: eureka-server cloud: config: fail-fast: true
يوجد باقي تكوين التطبيق في
eureka-server.yml
في موارد خادم التكوين:
server: port: 8761 eureka: client: register-with-eureka: true fetch-registry: false
يستخدم خادم Eureka المنفذ 8761 ، والذي يسمح لجميع عملاء Eureka بعدم تحديده باستخدام القيمة الافتراضية. تعني قيمة
register-with-eureka
(المشار إليها للتوضيح ،
register-with-eureka
يتم استخدامها أيضًا بشكل افتراضي) أن التطبيق نفسه ، مثله مثل خدمات microservices الأخرى ، سيتم تسجيله في خادم Eureka. تحدد معلمة
fetch-registry
ما إذا كان عميل Eureka سيتلقى بيانات من سجل الخدمة.
تتوفر قائمة بالطلبات المسجلة وغيرها من المعلومات على
http://localhost:8761/
:
بدائل تطبيق اكتشاف الخدمة هي القنصل و Zookeeper وغيرها.
خدمة البنود
هذا التطبيق هو مثال على النهاية الخلفية مع تطبيق REST API باستخدام إطار WebFlux الذي ظهر في Spring 5 (الوثائق
هنا ) ، أو بالأحرى Kotlin DSL له:
@Bean fun itemsRouter(handler: ItemHandler) = router { path("/items").nest { GET("/", handler::getAll) POST("/", handler::add) GET("/{id}", handler::getOne) PUT("/{id}", handler::update) } }
معالجة طلبات HTTP المستلمة يتم تفويضها إلى فئة برامج
ItemHandler
. على سبيل المثال ، طريقة للحصول على قائمة كائنات بعض الكيان تبدو كما يلي:
fun getAll(request: ServerRequest) = ServerResponse.ok() .contentType(APPLICATION_JSON_UTF8) .body(fromObject(itemRepository.findAll()))
يصبح التطبيق عميل خادم Eureka ، أي أنه يسجل ويستقبل البيانات من سجل الخدمة ، بسبب وجود
spring-cloud-starter-netflix-eureka-client
. بعد التسجيل ، يرسل التطبيق وحدات hartbits إلى خادم Eureka بتردد معين ، وإذا كانت نسبة البتات التي يستلمها خادم Eureka لفترة زمنية محددة تنخفض عن عتبة معينة ، فسيتم حذف التطبيق من سجل الخدمة.
ضع في اعتبارك إحدى الطرق لإرسال بيانات تعريف إضافية إلى خادم Eureka:
@PostConstruct private fun addMetadata() = aim.registerAppMetadata(mapOf("description" to "Some description"))
تأكد من تلقي هذه البيانات بواسطة خادم Eureka من خلال الانتقال إلى
http://localhost:8761/eureka/apps/items-service
عبر Postman:
عناصر واجهة المستخدم
تؤدي هذه الخدمة المصغرة ، بالإضافة إلى إظهار التفاعل مع بوابة واجهة المستخدم (ستظهر في القسم التالي) ، وظيفة الواجهة الأمامية لخدمة العناصر ، والتي يمكن أن تتفاعل مع واجهة برمجة تطبيقات REST بعدة طرق:
- العميل إلى REST API مكتوب باستخدام OpenFeign:
@FeignClient("items-service", fallbackFactory = ItemsServiceFeignClient.ItemsServiceFeignClientFallbackFactory::class) interface ItemsServiceFeignClient { @GetMapping("/items/{id}") fun getItem(@PathVariable("id") id: Long): String @GetMapping("/not-existing-path") fun testHystrixFallback(): String @Component class ItemsServiceFeignClientFallbackFactory : FallbackFactory<ItemsServiceFeignClient> { private val log = LoggerFactory.getLogger(this::class.java) override fun create(cause: Throwable) = object : ItemsServiceFeignClient { override fun getItem(id: Long): String { log.error("Cannot get item with id=$id") throw ItemsUiException(cause) } override fun testHystrixFallback(): String { log.error("This is expected error") return "{\"error\" : \"Some error\"}" } } } }
- فول
RestTemplate
يتم إنشاء سلة في java config:
@Bean @LoadBalanced fun restTemplate() = RestTemplate()
واستخدمت بهذه الطريقة:
fun requestWithRestTemplate(id: Long): String = restTemplate.getForEntity("http://items-service/items/$id", String::class.java).body ?: "No result"
WebClient
فئة WebClient
(طريقة خاصة بإطار WebFlux)
يتم إنشاء سلة في java config:
@Bean fun webClient(loadBalancerClient: LoadBalancerClient) = WebClient.builder() .filter(LoadBalancerExchangeFilterFunction(loadBalancerClient)) .build()
واستخدمت بهذه الطريقة:
fun requestWithWebClient(id: Long): Mono<String> = webClient.get().uri("http://items-service/items/$id").retrieve().bodyToMono(String::class.java)
يمكن التحقق من حقيقة أن كل الطرق الثلاث ترجع نفس النتيجة من خلال الانتقال إلى
http://localhost:8081/example
:
أفضل الخيار باستخدام OpenFeign ، لأنه يجعل من الممكن تطوير عقد للتفاعل مع microservice ، والذي يتم تنفيذه بواسطة Spring. يتم حقن الكائن الذي ينفذ هذا العقد ويستخدم مثل الحبة العادية:
itemsServiceFeignClient.getItem(1)
إذا فشل الطلب لسبب ما ، فسيتم استدعاء الأسلوب المقابل للفئة التي تنفذ واجهة
FallbackFactory
، والتي تحتاج إلى معالجة الخطأ وإرجاع الاستجابة الافتراضية (أو رمي استثناء إضافي). في حالة فشل عدد معين من المكالمات المتتالية ، سيفتح Fuse الدائرة (المزيد حول قواطع الدائرة
هنا وهنا ) ، مما يتيح الوقت لاستعادة الخدمة الميكروية الساقطة.
لاستخدام عميل Feign ، يلزمك إضافة تعليق توضيحي إلى
@EnableFeignClients
التطبيق
@EnableFeignClients
:
@SpringBootApplication @EnableFeignClients(clients = [ItemsServiceFeignClient::class]) class ItemsUiApplication
لكي تعمل أداة الإرجاع Hystrix في عميل Feign ، تحتاج إلى إضافة ما يلي إلى تهيئة التطبيق:
feign: hystrix: enabled: true
لاختبار تشغيل Hbackrix backback في عميل Feign ، ما عليك سوى الانتقال إلى
http://localhost:8081/hystrix-fallback
. سيحاول عميل Feign تنفيذ الطلب على مسار غير موجود في خدمة العناصر ، مما سيؤدي إلى عودة الاستجابة:
{"error" : "Some error"}
بوابة واجهة المستخدم
يتيح لك نمط عبّارة واجهة برمجة التطبيقات إنشاء نقطة إدخال واحدة لواجهة برمجة التطبيقات التي توفرها خدمات microservices الأخرى (مزيد من التفاصيل
هنا ). يقوم أحد التطبيقات التي تنفذ هذا النمط بإجراء توجيه (توجيه) لطلبات الخدمات الصغيرة ، ويمكنه أيضًا تنفيذ وظائف إضافية ، على سبيل المثال ، المصادقة.
في هذا المشروع ، لمزيد من الوضوح ، يتم تنفيذ بوابة واجهة المستخدم ، أي نقطة دخول واحدة لوحدات واجهة مستخدم مختلفة ؛ من الواضح ، أن بوابة API يتم تنفيذها بالمثل. يتم تنفيذ الخدمة المصغرة على أساس إطار Spring Cloud Gateway. البديل هو Netflix Zuul ، وهو جزء من Netflix OSS ومتكامل مع Spring Boot باستخدام Spring Cloud Netflix.
تعمل بوابة واجهة المستخدم على المنفذ 443 باستخدام شهادة SSL التي تم إنشاؤها (الموجودة في المشروع). تم تكوين SSL و HTTPS على النحو التالي:
server: port: 443 ssl: key-store: classpath:keystore.p12 key-store-password: qwerty key-alias: test_key key-store-type: PKCS12
يتم تخزين تسجيلات دخول المستخدم وكلمات المرور في تطبيق قائم على الخريطة لواجهة
ReactiveUserDetailsService
الخاصة بـ WebFlux:
@Bean fun reactiveUserDetailsService(): ReactiveUserDetailsService { val user = User.withDefaultPasswordEncoder() .username("john_doe").password("qwerty").roles("USER") .build() val admin = User.withDefaultPasswordEncoder() .username("admin").password("admin").roles("ADMIN") .build() return MapReactiveUserDetailsService(user, admin) }
يتم تكوين إعدادات الأمان على النحو التالي:
@Bean fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain = http .formLogin().loginPage("/login") .and() .authorizeExchange() .pathMatchers("/login").permitAll() .pathMatchers("/static/**").permitAll() .pathMatchers("/favicon.ico").permitAll() .pathMatchers("/webjars/**").permitAll() .pathMatchers("/actuator/**").permitAll() .anyExchange().authenticated() .and() .csrf().disable() .build()
يحدد التكوين المحدد أن جزءًا من موارد الويب (على سبيل المثال ، الإحصائيات) متاح لجميع المستخدمين ، بما في ذلك أولئك الذين لم تتم المصادقة ، وكل شيء آخر (
.anyExchange()
) مصادق عليه فقط. إذا حاولت إدخال عنوان URL يتطلب المصادقة ، فسيتم إعادة توجيهه إلى صفحة تسجيل الدخول (
https://localhost/login
):
تستخدم هذه الصفحة أدوات إطار عمل Bootstrap ، والذي يتصل بالمشروع باستخدام Webjars ، مما يجعل من الممكن إدارة المكتبات من جانب العميل كتبعيات منتظمة. يستخدم الزعتر لتشكيل صفحات HTML. تم تكوين الوصول إلى صفحة تسجيل الدخول باستخدام WebFlux:
@Bean fun routes() = router { GET("/login") { ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("login") } }
يمكن تكوين توجيه Spring Cloud Gateway بتكوين YAML أو java. يتم تعيين المسارات إلى الخدمات الصغيرة إما يدويًا أو يتم إنشاؤها تلقائيًا استنادًا إلى البيانات الواردة من سجل الخدمة. مع وجود عدد كبير بما فيه الكفاية من واجهات المستخدم التي تتطلب التوجيه ، سيكون من الأنسب استخدام التكامل مع سجل الخدمة:
spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true include-expression: serviceId.endsWith('-UI') url-expression: "'lb:http://'+serviceId"
تشير قيمة معلمة
include-expression
إلى أنه سيتم إنشاء التوجيهات فقط للخدمات الميكروية التي تنتهي أسماؤها في
واجهة المستخدم ، وقيمة معلمة
url-expression
هي أنه يمكن الوصول إليها عبر بروتوكول HTTP ، على عكس بوابة واجهة المستخدم التي تعمل عبر HTTPS ، وعند الوصول إليها سوف يستخدمون موازنة تحميل العميل (يتم تنفيذها باستخدام Netflix Ribbon).
فكر في مثال إنشاء طرق في java config يدويًا (بدون تكامل مع سجل الخدمة):
@Bean fun routeLocator(builder: RouteLocatorBuilder) = builder.routes { route("eureka-gui") { path("/eureka") filters { rewritePath("/eureka", "/") } uri("lb:http://eureka-server") } route("eureka-internals") { path("/eureka/**") uri("lb:http://eureka-server") } }
طرق التوجيه الأولى إلى الصفحة الرئيسية لخادم Eureka المعروضة سابقًا (
http://localhost:8761
) ، والثاني مطلوب لتحميل الموارد على هذه الصفحة.
تتوفر جميع المسارات التي أنشأها التطبيق على
https://localhost/actuator/gateway/routes
.
في الخدمات المصغرة الأساسية ، قد يكون من الضروري الوصول إلى تسجيل الدخول و / أو أدوار المستخدم المصادق عليها في بوابة واجهة المستخدم. للقيام بذلك ، قمت بإنشاء عامل تصفية يضيف الرؤوس المناسبة إلى الطلب:
@Component class AddCredentialsGlobalFilter : GlobalFilter { private val loggedInUserHeader = "logged-in-user" private val loggedInUserRolesHeader = "logged-in-user-roles" override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain) = exchange.getPrincipal<Principal>() .flatMap { val request = exchange.request.mutate() .header(loggedInUserHeader, it.name) .header(loggedInUserRolesHeader, (it as Authentication).authorities?.joinToString(";") ?: "") .build() chain.filter(exchange.mutate().request(request).build()) } }
الآن دعنا ننتقل إلى واجهة مستخدم العناصر باستخدام بوابة واجهة المستخدم -
https://localhost/items-ui/greeting
، على افتراض أن معالجة هذه الرؤوس قد تم تنفيذها بالفعل في عناصر واجهة المستخدم:
Spring Cloud Sleuth هو حل لتتبع الاستعلام في نظام موزع. تتم إضافة معرف التتبع (معرف التمرير) و Span Id (وحدة معرف العمل) إلى رؤوس الطلب التي تمر عبر العديد من الخدمات المصغرة (لتسهيل الفهم ، قمت بتبسيط المخطط ؛ وفيما يلي شرح أكثر تفصيلًا):
هذه الوظيفة متصلة ببساطة عن طريق إضافة
spring-cloud-starter-sleuth
.
من خلال تحديد إعدادات التسجيل المناسبة ، في وحدة التحكم الخاصة بالخدمات المصاحبة المقابلة ، يمكنك رؤية شيء مثل التالي (معرّف التتبع ومعرف Span معروضان بعد اسم الخدمة المصغرة):
DEBUG [ui-gateway,009b085bfab5d0f2,009b085bfab5d0f2,false] oscghRoutePredicateHandlerMapping : Route matched: CompositeDiscoveryClient_ITEMS-UI DEBUG [items-ui,009b085bfab5d0f2,947bff0ce8d184f4,false] oswrfunction.server.RouterFunctions : Predicate "(GET && /example)" matches against "GET /example" DEBUG [items-service,009b085bfab5d0f2,dd3fa674cd994b01,false] oswrfunction.server.RouterFunctions : Predicate "(GET && /{id})" matches against "GET /1"
للحصول على تمثيل رسومي للتتبع الموزع ، يمكنك استخدام Zipkin ، على سبيل المثال ، والذي سيكون بمثابة خادم يقوم بتجميع المعلومات حول طلبات HTTP من خدمات microservices الأخرى (مزيد من التفاصيل
هنا ).
الجمعية
اعتمادًا على نظام التشغيل ، يتم
gradlew clean build
./gradlew clean build
أو
./gradlew clean build
.
نظرًا لإمكانية استخدام برنامج
Gradle wrapper ، ليست هناك حاجة إلى برنامج Gradle مثبت محليًا.
بناء والإطلاق اللاحق بنجاح على JDK 11.0.1. قبل ذلك ، كان المشروع يعمل على JDK 10 ، لذلك افترض أنه لن يكون هناك أي مشكلة في التجميع والإطلاق على هذا الإصدار. ليس لدي أي بيانات حول الإصدارات السابقة من JDK. أيضًا ، ضع في اعتبارك أن Gradle 5 المستخدمة تتطلب JDK 8 على الأقل.
إطلاق
أوصي ببدء تشغيل التطبيقات بالترتيب الموضح في هذه المقالة. إذا كنت تستخدم Intellij IDEA مع تمكين Run Dashboard ، فيجب أن تحصل على شيء مثل التالي:
الخاتمة
درس المقال مثالًا على بنية الخدمات المصغرة على حزمة التكنولوجيا الحالية في عالم Java ومكوناته الرئيسية وبعض الميزات. آمل أن تكون المادة مفيدة. شكرا لاهتمامكم!
المراجع