# თავი 12: შეტყობინებები (Messaging) და ასინქრონული პროცესინგი (Asynchronous Processing) --- ## 12.1 მოთხოვნა-პასუხის (Request-Response) მიღმა წინა თავებში განხილული ყოველი ინტერაქცია ერთსა და იმავე პატერნს მიჰყვებოდა: კლიენტი აგზავნის მოთხოვნას (request), სერვერი ამუშავებს მას და სერვერი აბრუნებს პასუხს (response). კლიენტი ელოდება მთლიანი ოპერაციის დასრულებას. ეს არის **სინქრონული** პროცესინგი — პირდაპირი, მარტივად გასაგები და სრულიად ადეკვატური ვებ-ოპერაციების უმეტესობისთვის. მაგრამ ზოგიერთი ამოცანა ამ მოდელს არ ერგება. შეკვეთის გაფორმების შემდეგ დადასტურების ელფოსტის გაგზავნამ მომხმარებელს არ უნდა აცდევინოს 3 წამი SMTP სერვერის პასუხამდე. დიდი ფაილის ატვირთვის დამუშავებამ არ უნდა დაბლოკოს HTTP Thread-ი 30 წამის განმავლობაში. ფასის ცვლილების შესახებ ათეულობით დამოკიდებული სერვისის (downstream services) ინფორმირება არ უნდა მოხდეს იმ API გამოძახების პარალელურად, რომელმაც ეს გამოიწვია. ეს თავი წარმოგიდგენთ **ასინქრონულ პროცესინგს** — ამოცანების ფონურ რეჟიმში გაშვებას, რათა გამომძახებელმა (caller) არ მოიცადოს — და **messaging**-ს — კომპონენტებს ან სისტემებს შორის კომუნიკაციას message broker-ის (შეტყობინებების ბროკერის) მეშვეობით, პირდაპირი მეთოდების გამოძახების ნაცვლად. ჩვენ ვისწავლით, თუ როგორ გავუშვათ ფონური ამოცანები `@Async`-ის გამოყენებით, გამოვაქვეყნოთ და მოვუსმინოთ შიდა სააპლიკაციო ივენთებს (events), გავაგზავნოთ და მივიღოთ შეტყობინებები JMS-ისა და ActiveMQ-ს მეშვეობით, დავგეგმოთ განმეორებადი ამოცანები `@Scheduled`-ით, გავაგზავნოთ ელფოსტები `JavaMailSender`-ით და განვახორციელოთ რეალურ დროში კომუნიკაცია WebSockets-ის დახმარებით. --- ## 12.2 სინქრონული და ასინქრონული პროცესინგი | ასპექტი | სინქრონული (Synchronous) | ასინქრონული (Asynchronous) | | --- | --- | --- | | შესრულება | ბლოკირებადი (Blocking) — გამომძახებელი ელოდება ოპერაციის დასრულებას. | არაბლოკირებადი (Non-blocking) — გამომძახებელი დაუყოვნებლივ აგრძელებს მუშაობას. | | პასუხი | შედეგი ხელმისაწვდომია გამოძახების დასრულებისთანავე. | შედეგი მოდის მოგვიანებით (ან საერთოდ არ არის შედეგი). | | Thread-ის გამოყენება | გამოძახების ხანგრძლივობის განმავლობაში დაკავებულია ერთი Thread-ი. | სამუშაო სრულდება განცალკევებულ Thread-ზე. | | მაგალითი | REST API, რომელიც მიმართავს ბაზას და აბრუნებს შედეგს. | ელფოსტის გაგზავნა მომხმარებლის რეგისტრაციის შემდეგ. | ასინქრონული დამუშავება მიზანშეწონილია, როდესაც ამოცანის დასრულებას დიდი დრო სჭირდება (ელფოსტის გაგზავნა, რეპორტების გენერირება, ნელ გარე სერვისებთან დაკავშირება), როდესაც გამომძახებელს შედეგი დაუყოვნებლივ არ სჭირდება, ან როდესაც გსურთ გამტარუნარიანობის (throughput) გაუმჯობესება მოთხოვნის დამმუშავებელი Thread-ების განთავისუფლებით. --- ## 12.3 Messaging კონცეფციები Messaging არის კომუნიკაციის პატერნი, სადაც კომპონენტები მონაცემებს ცვლიან ცენტრალური შუამავლის — **message broker**-ის — მეშვეობით და არა ერთმანეთის პირდაპირი გამოძახებით. ამ პატერნს ჰყავს სამი მონაწილე: **producer** ქმნის შეტყობინებას და აგზავნის მას ბროკერთან, **broker** ინახავს შეტყობინებას და ამისამართებს მას სწორ დანიშნულების ადგილზე, ხოლო **consumer** იღებს შეტყობინებას და ამუშავებს მას. არ არის აუცილებელი, რომ პროდიუსერი და კონსიუმერი ერთდროულად მუშაობდნენ, არ არის აუცილებელი, რომ ერთმანეთის შესახებ იცოდნენ და არ არის აუცილებელი, რომ ერთსა და იმავე პროგრამულ ენაზე იყვნენ დაწერილნი. Messaging სისტემები იყენებენ მიწოდების ორ მოდელს: | მოდელი | ქცევა | ანალოგია | | --- | --- | --- | | **Queue** (რიგი) | ერთი შეტყობინება მიეწოდება ზუსტად ერთ კონსიუმერს. თუ ერთსა და იმავე რიგს რამდენიმე კონსიუმერი უსმენს, შეტყობინებები ნაწილდება round-robin პრინციპით. | წერილი — მას მხოლოდ ერთი მიმღები კითხულობს. | | **Topic** | ერთი შეტყობინება მიეწოდება ყველა გამომწერს (subscriber). Topic-ის ყველა მსმენელი კონსიუმერი იღებს შეტყობინების ასლს. | მაუწყებლობა (Broadcast) — ყველა, ვინც ჩართულია, ისმენს მას. | ### Messaging ვარიანტები Spring Boot-ში | ინსტრუმენტი | ტიპი | საუკეთესოა შემდეგისთვის | | --- | --- | --- | | `@Async` | პროცესის შიდა ფონური Thread-ები | მარტივი ასინქრონული ლოგიკა ერთი აპლიკაციის ფარგლებში. | | Spring Events | პროცესის შიდა ივენთების გამოქვეყნება | კომპონენტების დამოუკიდებლობა (Decoupling) ერთი აპლიკაციის ფარგლებში. | | JMS (ActiveMQ) | გარე message broker | ენტერპრაიზ სისტემები, სანდო რიგები (reliable queuing), Java-to-Java კომუნიკაცია. | | RabbitMQ | გარე message broker (AMQP) | სანდო რიგები, მიკროსერვისები. | | Kafka | განაწილებული სტრიმინგ პლატფორმა | მაღალი გამტარუნარიანობის მქონე ივენთების სტრიმინგი, ლოგების აგრეგაცია. | ეს თავი სიღრმისეულად მოიცავს `@Async`-ს, Spring Events-ს და JMS-ს ActiveMQ-სთან ერთად. RabbitMQ და Kafka იყენებენ მსგავს საინტეგრაციო პატერნებს — Spring Boot უზრუნველყოფს starter-ებს ორივესთვის — მაგრამ მათი კონფიგურაცია და ოპერაციული მახასიათებლები განსხვავებულია და სცდება ამ თავის ფარგლებს. --- ## 12.4 ფონური ამოცანები (Background Tasks) @Async-ით `@Async` ანოტაცია მეთოდს უშვებს განცალკევებულ Thread-ზე. გამომძახებელი დაუყოვნებლივ აგრძელებს მუშაობას, მეთოდის დასრულების მოლოდინის გარეშე. ### Async მხარდაჭერის ჩართვა ```java @Configuration @EnableAsync public class AsyncConfig { } ``` `@EnableAsync` ააქტიურებს Spring-ის ასინქრონული მეთოდების შესრულების მხარდაჭერას. მის გარეშე, `@Async` ანოტაციები მეთოდებზე უბრალოდ იგნორირებულია. ### Fire-and-Forget (გაუშვი-და-დაივიწყე) მეთოდები ყველაზე მარტივი async პატერნი არის `void` მეთოდი — გაუშვით ამოცანა და დაივიწყეთ მის შესახებ: ```java @Service public class NotificationService { private static final Logger log = LoggerFactory.getLogger(NotificationService.class); @Async public void sendNotification(String userId, String message) { log.info("Sending notification on thread: {}", Thread.currentThread().getName()); // Simulate slow operation try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("Notification sent to user: {}", userId); } } ``` როდესაც Controller იძახებს `notificationService.sendNotification(...)`-ს, მეთოდი ბრუნდება დაუყოვნებლივ და რეალური სამუშაო სრულდება ფონურ Thread-ზე. მომხმარებლის HTTP პასუხი არ ყოვნდება. ### მეთოდები, რომლებიც აბრუნებენ შედეგს როდესაც გჭირდებათ ასინქრონული ოპერაციის შედეგი, დააბრუნეთ `CompletableFuture`: ```java @Async public CompletableFuture fetchDataFromExternalService() { log.info("Fetching data on thread: {}", Thread.currentThread().getName()); String result = restClient.get().uri("/data").retrieve().body(String.class); return CompletableFuture.completedFuture(result); } ``` `CompletableFuture.completedFuture(result)` ახვევს (wraps) შედეგს `CompletableFuture`-ში, რომელიც უკვე რეზოლვებულია (resolved). გამომძახებელს შეუძლია შეამოწმოს დასრულებულია თუ არა future, დაელოდოს შედეგს, ან დააკომპოზიციოს იგი სხვა future-ებთან. გაითვალისწინეთ, რომ `AsyncResult` (რომელსაც შესაძლოა ძველი ტუტორიალები და დოკუმენტაცია ეყრდნობოდეს) მოძველებულია (deprecated) Spring Framework 6.0-დან. ყოველთვის გამოიყენეთ `CompletableFuture`. ### რამდენიმე Async შედეგის კომბინირება `CompletableFuture` მხარს უჭერს რამდენიმე async გამოძახების შედეგების კომბინირებას: ```java @Service public class AggregationService { private final UserService userService; private final OrderService orderService; public AggregationService(UserService userService, OrderService orderService) { this.userService = userService; this.orderService = orderService; } public DashboardData getDashboardData(Long userId) throws Exception { CompletableFuture profileFuture = userService.getProfileAsync(userId); CompletableFuture> ordersFuture = orderService.getOrdersAsync(userId); // Wait for both to complete CompletableFuture.allOf(profileFuture, ordersFuture).join(); return new DashboardData(profileFuture.get(), ordersFuture.get()); } } ``` ორივე async გამოძახება სრულდება პარალელურად (concurrently). `CompletableFuture.allOf(...).join()` ბლოკავს პროცესს მანამ, სანამ ორივე არ დასრულდება. თუ პროფილის წამოღებას სჭირდება 2 წამი და შეკვეთების წამოღებას 3 წამი, ჯამური მოლოდინის დრო არის 3 წამი — და არა 5. ### მნიშვნელოვანი შეზღუდვები **`@Async` მუშაობს მხოლოდ public მეთოდებზე.** Spring ახორციელებს `@Async`-ს proxying-ის მეშვეობით. პროქსის შეუძლია გადაიჭიროს (intercept) მხოლოდ ის გამოძახებები, რომლებიც მოდის პროქსის გავლით — რაც ნიშნავს გამოძახებებს კლასის გარედან. Private მეთოდები და self-invocations (ერთი და იგივე კლასიდან `@Async` მეთოდის გამოძახება) გვერდს უვლის პროქსის და სრულდება სინქრონულად. **არ გამოიძახოთ `@Async` მეთოდი იმავე კლასიდან.** ეს არის ყველაზე გავრცელებული შეცდომა. თუ `ServiceA.methodA()` იძახებს `ServiceA.asyncMethodB()`-ს, async ანოტაცია იგნორირებულია, რადგან გამოძახება არ გადის პროქსიზე. გადაიტანეთ async მეთოდი ცალკეულ სერვისში. ### Thread Pool-ის კონფიგურირება ნაგულისხმევად, Spring იყენებს `SimpleAsyncTaskExecutor`-ს, რომელიც ქმნის ახალ Thread-ს ყოველი ამოცანისთვის. პროდაქშენისთვის (production), დააკონფიგურირეთ სათანადო Thread pool-ი: ```java @Configuration @EnableAsync public class AsyncConfig { @Bean(name = "taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-"); executor.initialize(); return executor; } } ``` მიუთითეთ ეგზეკუტორს (executor) სახელით, თუ გაქვთ რამდენიმე: ```java @Async("taskExecutor") public void processInBackground() { // Runs on the "taskExecutor" thread pool } ``` ### Exception Handling `void` Async მეთოდებისთვის როდესაც `CompletableFuture`-ის დამბრუნებელი async მეთოდი ისვრის ექსეფშენს (exception), გამომძახებელი ხედავს მას `.get()`-ის გამოძახებისას. მაგრამ `void` მეთოდებისთვის, ექსეფშენს წასასვლელი არსად აქვს. განახორციელეთ `AsyncUncaughtExceptionHandler` მათ დასაჭერად: ```java public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { private static final Logger log = LoggerFactory.getLogger(CustomAsyncExceptionHandler.class); @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { log.error("Async exception in method {}: {}", method.getName(), ex.getMessage(), ex); } } ``` დაარეგისტრირეთ ის `AsyncConfigurer`-ის იმპლემენტაციით: ```java @Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new CustomAsyncExceptionHandler(); } } ``` --- ## 12.5 Spring Application ივენთები (Events) Spring Events უზრუნველყოფს გზას ერთი და იგივე აპლიკაციის კომპონენტებისთვის, რომ დაუკავშირდნენ ერთმანეთს პირდაპირი დამოკიდებულების გარეშე. ერთი კომპონენტი აქვეყნებს (publishes) ივენთს და ნებისმიერი რაოდენობის მსმენელი (listener) რეაგირებს მასზე. ### ივენთის განსაზღვრა ```java public class UserRegisteredEvent extends ApplicationEvent { private final String email; public UserRegisteredEvent(Object source, String email) { super(source); this.email = email; } public String getEmail() { return email; } } ``` ### ივენთის გამოქვეყნება (Publishing) გააკეთეთ `ApplicationEventPublisher`-ის ინექცია (inject) და გამოიძახეთ `publishEvent`: ```java @Service public class UserService { private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; public UserService(UserRepository userRepository, ApplicationEventPublisher eventPublisher) { this.userRepository = userRepository; this.eventPublisher = eventPublisher; } public void registerUser(User user) { userRepository.save(user); eventPublisher.publishEvent(new UserRegisteredEvent(this, user.getEmail())); } } ``` ### ივენთების მოსმენა (Listening) ```java @Component public class WelcomeEmailListener { private final EmailService emailService; public WelcomeEmailListener(EmailService emailService) { this.emailService = emailService; } @EventListener public void handleUserRegistered(UserRegisteredEvent event) { emailService.sendWelcomeEmail(event.getEmail()); } } ``` `UserService`-მა არ იცის `WelcomeEmailListener`-ის შესახებ. ის უბრალოდ აქვეყნებს ივენთს. თუ მოგვიანებით დაამატებთ მეორე listener-ს, რომელიც ქმნის დეფოლტ პროფილს, ან მესამეს, რომელიც აგზავნის ანალიტიკის ივენთს, `UserService` არ იცვლება. ეს არის Observer პატერნი — ივენთები აცალკევებენ (decouple) ფაბლიშერს (publisher) გამომწერებისგან (subscribers). ნაგულისხმევად, `@EventListener` მეთოდები სრულდება სინქრონულად იმავე Thread-ზე. ივენთების ასინქრონულად დასამუშავებლად, დაამატეთ `@Async`: ```java @Async @EventListener public void handleUserRegistered(UserRegisteredEvent event) { emailService.sendWelcomeEmail(event.getEmail()); } ``` --- ## 12.6 Message Brokers და JMS როდესაც გჭირდებათ messaging განცალკევებულ აპლიკაციებს შორის — ან როდესაც შეტყობინებები უნდა გადაურჩეს აპლიკაციის გადატვირთვებს — გჭირდებათ გარე message broker. **JMS (Java Message Service)** არის სტანდარტული API ჯავაში შეტყობინებების გაგზავნის, მიღებისა და დამუშავებისთვის. **Spring JMS** ამარტივებს JMS-თან მუშაობას `JmsTemplate`-ის (გასაგზავნად) და `@JmsListener`-ის (მისაღებად) მეშვეობით. **Apache ActiveMQ** არის პოპულარული JMS-თავსებადი ბროკერი. Spring Boot უზრუნველყოფს starter დამოკიდებულებებს (dependencies) როგორც ActiveMQ Classic-ისთვის (5.x), ასევე ActiveMQ Artemis-ისთვის. ### Setup (კონფიგურაცია) დაამატეთ ActiveMQ starter-ი და ბროკერის დამოკიდებულებები: ```xml org.springframework.boot spring-boot-starter-activemq org.apache.activemq activemq-broker runtime ``` დეველოპმენტისთვის (development), ჩაშენებული (embedded) ბროკერი ეშვება თქვენი აპლიკაციის შიგნით — გარე ინსტალაცია არ არის საჭირო. დააკონფიგურირეთ ის `application.properties`-ში: ```properties spring.activemq.broker-url=vm://localhost?broker.persistent=false spring.activemq.packages.trust-all=true ``` ### შეტყობინებების გაგზავნა JmsTemplate-ით ```java @Service public class OrderProducer { private final JmsTemplate jmsTemplate; public OrderProducer(JmsTemplate jmsTemplate) { this.jmsTemplate = jmsTemplate; } public void sendOrder(Order order) { jmsTemplate.convertAndSend("order-queue", order); log.info("Sent order to queue: {}", order.getId()); } } ``` `convertAndSend` ახდენს ობიექტის სერიალიზაციას (serialize) და აგზავნის მას დასახელებულ დანიშნულების ადგილზე. ნაგულისხმევად, გამოიყენება Java serialization, რაც მოითხოვს, რომ ობიექტს ჰქონდეს იმპლემენტირებული `Serializable`. ### შეტყობინებების მიღება @JmsListener-ით ```java @Component public class OrderConsumer { private static final Logger log = LoggerFactory.getLogger(OrderConsumer.class); @JmsListener(destination = "order-queue") public void processOrder(Order order) { log.info("Received order: {}", order.getId()); // Process the order } } ``` `@JmsListener` ავტომატურად უსმენს მითითებულ რიგს (queue). როდესაც შეტყობინება მოდის, Spring ახდენს მის დესერიალიზაციას და იძახებს მეთოდს. თუ რამდენიმე კონსიუმერი უსმენს ერთსა და იმავე რიგს, შეტყობინებები ნაწილდება round-robin პრინციპით. ### JSON სერიალიზაცია (Serialization) Java-ს სერიალიზაცია მყიფეა (fragile) — კლასის ცვლილებებმა შეიძლება დააზიანოს დესერიალიზაცია და ის მუშაობს მხოლოდ Java აპლიკაციებს შორის. JSON უფრო მძლავრი და თავსებადია (interoperable). დააკონფიგურირეთ JSON შეტყობინებების კონვერტორი: ```java @Configuration public class JmsConfig { @Bean public MappingJackson2MessageConverter jacksonJmsMessageConverter() { MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); converter.setTargetType(MessageType.TEXT); converter.setTypeIdPropertyName("_type"); return converter; } } ``` ამ კონფიგურაციით, შეტყობინებები იგზავნება როგორც JSON ტექსტი. `_type` property ჰედერი (header) ეუბნება მიმღებს, რომელ Java კლასში უნდა მოხდეს მისი დესერიალიზაცია. ### Topics (Publish-Subscribe) რიგების (queues) ნაცვლად topic მოდელის გამოსაყენებლად, დააკონფიგურირეთ listener factory და template `setPubSubDomain(true)`-ით: ```java @Configuration @EnableJms public class JmsTopicConfig { @Bean public DefaultJmsListenerContainerFactory topicListenerFactory( ConnectionFactory connectionFactory, MappingJackson2MessageConverter messageConverter) { DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setPubSubDomain(true); factory.setMessageConverter(messageConverter); return factory; } @Bean("topicJmsTemplate") public JmsTemplate topicJmsTemplate(ConnectionFactory connectionFactory, MappingJackson2MessageConverter messageConverter) { JmsTemplate template = new JmsTemplate(connectionFactory); template.setPubSubDomain(true); template.setMessageConverter(messageConverter); return template; } } ``` მსმენელის (listener) მხარეს, მიუთითეთ topic factory: ```java @JmsListener(destination = "price-updates", containerFactory = "topicListenerFactory") public void handlePriceUpdate(PriceUpdate update) { log.info("Received price update: {}", update); } ``` ყოველი კონსიუმერი, რომელიც გამოწერილია `price-updates` topic-ზე, იღებს თითოეულ შეტყობინებას — განსხვავებით რიგისგან (queue), სადაც თითოეული შეტყობინება მიდის მხოლოდ ერთ კონსიუმერთან. --- ## 12.7 ამოცანების დაგეგმვა (Scheduling) @Scheduled-ით ზოგიერთ ამოცანას სჭირდება განმეორებადი გრაფიკით გაშვება — ვადაგასული სესიების გასუფთავება, ყოველდღიური რეპორტების გაგზავნა, ქეშირებული (cached) მონაცემების განახლება. Spring უზრუნველყოფს `@Scheduled`-ს ამისთვის. ### Scheduling-ის ჩართვა ```java @Configuration @EnableScheduling public class SchedulingConfig { } ``` ### დაგეგმვის პატერნები (Scheduling Patterns) **Fixed rate** — ეშვება ყოველ N მილიწამში, მიუხედავად იმისა, თუ როდის დასრულდა წინა შესრულება: ```java @Component public class ScheduledTasks { private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class); @Scheduled(fixedRate = 60000) public void cleanupExpiredSessions() { log.info("Running session cleanup at {}", LocalDateTime.now()); } } ``` **Fixed delay** — იცდის N მილიწამს წინა შესრულების დასრულების შემდეგ, სანამ კვლავ გაეშვება: ```java @Scheduled(fixedDelay = 30000) public void pollExternalService() { // Runs 30 seconds after the previous execution finishes } ``` **Initial delay** — ამატებს შეყოვნებას (delay) პირველ გაშვებამდე: ```java @Scheduled(fixedRate = 60000, initialDelay = 10000) public void warmUpCache() { // First run after 10 seconds, then every 60 seconds } ``` **Cron expressions** — უზრუნველყოფს დეტალურ კონტროლს cron სინტაქსის გამოყენებით: ```java @Scheduled(cron = "0 0 9 * * MON-FRI") public void sendDailyReport() { // Runs at 9:00 AM every weekday } ``` ხშირი cron პატერნები: `"0 0 * * * *"` (ყოველ საათში), `"0 0 9 * * *"` (ყოველდღე დილის 9 საათზე), `"0 0/30 * * * *"` (ყოველ 30 წუთში), `"0 0 9 * * MON-FRI"` (სამუშაო დღეებში დილის 9 საათზე). განსხვავება `fixedRate`-სა და `fixedDelay`-ს შორის მნიშვნელოვანია, როდესაც ამოცანას იმაზე მეტი დრო სჭირდება, ვიდრე ინტერვალია. `fixedRate`-ის შემთხვევაში, შემდეგი შესრულება იწყება წინა შესრულების *დაწყებიდან* N მილიწამის შემდეგ — შესრულებები შეიძლება გადაფარონ ერთმანეთი, თუ ამოცანა ნელია. `fixedDelay`-ის შემთხვევაში, შემდეგი შესრულება იწყება წინა შესრულების *დასრულებიდან* N მილიწამის შემდეგ — არანაირი გადაფარვა, მაგრამ დაწყებებს შორის ინტერვალი ცვალებადია. --- ## 12.8 ელფოსტის გაგზავნა JavaMailSender-ით Spring Boot უზრუნველყოფს ელფოსტის მხარდაჭერას `spring-boot-starter-mail`-ის მეშვეობით: ```xml org.springframework.boot spring-boot-starter-mail ``` ### SMTP კონფიგურაცია ```properties spring.mail.host=smtp.gmail.com spring.mail.port=587 spring.mail.username=${MAIL_USERNAME} spring.mail.password=${MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true ``` ავტორიზაციის მონაცემები (Credentials) მოდის გარემოს ცვლადებიდან (environment variables) (თავი 5) — არასოდეს დაჰარდკოდოთ (hardcode) ისინი property ფაილებში. ### მარტივი ელფოსტის გაგზავნა ```java @Service public class EmailService { private final JavaMailSender mailSender; public EmailService(JavaMailSender mailSender) { this.mailSender = mailSender; } public void sendSimpleEmail(String to, String subject, String body) { SimpleMailMessage message = new SimpleMailMessage(); message.setTo(to); message.setSubject(subject); message.setText(body); message.setFrom("noreply@example.com"); mailSender.send(message); } } ``` ### HTML ელფოსტის გაგზავნა FreeMarker Template-ებით მდიდარი HTML ელფოსტებისთვის, გამოიყენეთ FreeMarker, რათა დაარენდეროთ (render) ელფოსტის სხეული (body) თემფლეითიდან (template). შექმენით თემფლეითი აქ: `src/main/resources/templates/email/welcome.ftlh`: ```html

Welcome, ${userName}!

Thank you for registering on our platform.

Your account has been created successfully.

Click here to activate your account ``` დაამუშავეთ თემფლეითი და გააგზავნეთ ელფოსტა: ```java @Service public class EmailService { private final JavaMailSender mailSender; private final FreeMarkerConfigurer freeMarkerConfigurer; public EmailService(JavaMailSender mailSender, FreeMarkerConfigurer freeMarkerConfigurer) { this.mailSender = mailSender; this.freeMarkerConfigurer = freeMarkerConfigurer; } public void sendWelcomeEmail(String to, String userName, String activationLink) throws Exception { Template template = freeMarkerConfigurer.getConfiguration() .getTemplate("email/welcome.ftlh"); Map model = Map.of( "userName", userName, "activationLink", activationLink); String htmlContent = FreeMarkerTemplateUtils.processTemplateIntoString(template, model); MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); helper.setTo(to); helper.setSubject("Welcome to Our Platform!"); helper.setText(htmlContent, true); helper.setFrom("noreply@example.com"); mailSender.send(message); } } ``` ### ელფოსტის ასინქრონულად გაგზავნა ელფოსტის გაგზავნა `@Async`-ის გამოყენების კლასიკური კანდიდატია — მომხმარებელი არ უნდა ელოდოს SMTP ტრანზაქციას: ```java @Async public void sendWelcomeEmailAsync(String to, String userName, String activationLink) { try { sendWelcomeEmail(to, userName, activationLink); } catch (Exception e) { log.error("Failed to send welcome email to {}: {}", to, e.getMessage()); } } ``` --- ## 12.9 რეალურ დროში კომუნიკაცია WebSockets-ით აქამდე განხილული ყველა საკომუნიკაციო პატერნი ინიცირებულია კლიენტის მიერ — კლიენტი აგზავნის მოთხოვნას, სერვერი პასუხობს. **WebSockets** უზრუნველყოფს სრულ-დუპლექსურ (full-duplex) კომუნიკაციას ერთი TCP კავშირის გავლით — როგორც კლიენტს, ისე სერვერს შეუძლია შეტყობინებების გაგზავნა ნებისმიერ დროს. ეს შესაძლებელს ხდის რეალურ დროში ისეთ ფუნქციებს, როგორიცაა ჩატის აპლიკაციები, ლაივ ნოტიფიკაციები, კოლაბორაციული რედაქტირება და ლაივ დეშბორდები. ### Setup ```xml org.springframework.boot spring-boot-starter-websocket ``` ### კონფიგურაცია STOMP-ით და SockJS-ით **STOMP** (Simple Text Oriented Messaging Protocol) უზრუნველყოფს publish-subscribe სემანტიკას WebSockets-ის ზემოთ. **SockJS** უზრუნველყოფს ფოლბექს (fallback) იმ ბრაუზერებისთვის, რომლებსაც არ აქვთ WebSockets-ის მხარდაჭერა. ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws").withSockJS(); } } ``` `/topic` არის პრეფიქსი სერვერიდან გამოწერილი კლიენტებისთვის გაგზავნილი შეტყობინებებისთვის. `/app` არის პრეფიქსი კლიენტიდან სერვერისკენ გაგზავნილი შეტყობინებებისთვის (რომლებსაც ამუშავებს `@MessageMapping`). `/ws` არის WebSocket-ის კავშირის endpoint-ი. ### Message Controller ```java @Controller public class ChatController { @MessageMapping("/chat.send") @SendTo("/topic/messages") public ChatMessage sendMessage(@Payload ChatMessage message) { return message; } } ``` როდესაც კლიენტი აგზავნის შეტყობინებას `/app/chat.send`-ზე, `sendMessage` მეთოდი ამუშავებს მას და `@SendTo` ანოტაცია აბროუდქასთებს (broadcasts) შედეგს ყველა კლიენტისთვის, ვინც გამოწერილია `/topic/messages`-ზე. ### JavaScript Client ```html ``` კლიენტი უკავშირდება `/ws` endpoint-ს, იწერს `/topic/messages`-ს ბროუდქასთების მისაღებად და აგზავნის შეტყობინებებს `/app/chat.send`-ზე. სერვერის მხარეს არსებული Controller იღებს შეტყობინებას და უგზავნის მას უკან (broadcasts) ყველა გამომწერს. --- ## შეჯამება (Summary) ამ თავმა მოიცვა ასინქრონული პროცესინგის და messaging-ის პატერნები Spring Boot-ში. **სინქრონული vs. ასინქრონული პროცესინგი:** სინქრონული დამუშავება ბლოკავს გამომძახებელს ოპერაციის დასრულებამდე; ასინქრონული დამუშავება ბრუნდება დაუყოვნებლივ და სამუშაოს ასრულებს ფონურ რეჟიმში. **`@Async`** უშვებს მეთოდებს ფონურ Thread-ებზე. ის მოითხოვს `@EnableAsync`-ს, მუშაობს მხოლოდ public მეთოდებზე, რომლებიც გამოძახებულია კლასის გარედან და დაბრუნებული მნიშვნელობებისთვის უნდა იყენებდეს `CompletableFuture`-ს. პროდაქშენისთვის (production) უნდა დაკონფიგურირდეს ქასთომ (custom) Thread pool-ები. **Spring Events** აცალკევებს (decouples) კომპონენტებს აპლიკაციის შიგნით Observer პატერნის მეშვეობით. ფაბლიშერი აგზავნის ივენთს `ApplicationEventPublisher`-ის მეშვეობით და ნებისმიერი `@EventListener` მეთოდი, რომელიც ემთხვევა ივენთის ტიპს, რეაგირებს მასზე. **JMS ActiveMQ-სთან ერთად** უზრუნველყოფს სანდო messaging-ს აპლიკაციებს შორის ბროკერის (broker) მეშვეობით. `JmsTemplate` აგზავნის შეტყობინებებს, `@JmsListener` იღებს მათ. Queues (რიგები) აწვდიან თითოეულ შეტყობინებას ერთ კონსიუმერს; Topics აწვდიან ყველა გამომწერს (subscriber). უპირატესობა ენიჭება JSON სერიალიზაციას `MappingJackson2MessageConverter`-ის მეშვეობით, ვიდრე Java სერიალიზაციას. **`@Scheduled`** უშვებს მეთოდებს განმეორებადი გრაფიკით. `fixedRate` ეშვება ფიქსირებული ინტერვალებით, `fixedDelay` იცდის ყოველი დასრულების შემდეგ და cron expressions უზრუნველყოფს დეტალურ კონტროლს. **JavaMailSender** აგზავნის ელფოსტებს, მარტივი ტექსტური შეტყობინებებიდან დაწყებული მდიდარი HTML-ით დასრულებული, რომელიც დარენდერებულია FreeMarker თემფლეითებიდან. ელფოსტის გაგზავნის `@Async`-თან კომბინირება თავიდან გარიდებთ მომხმარებლის მოთხოვნის (request) დაბლოკვას. **WebSockets** STOMP-თან და SockJS-თან ერთად უზრუნველყოფს რეალურ დროში, ორმხრივ (bidirectional) კომუნიკაციას სერვერსა და კლიენტებს შორის — შესაფერისია ჩატისთვის, ნოტიფიკაციებისთვის და ლაივ დეშბორდებისთვის. --- ## რესურსები (Resources) * [How To Do @Async in Spring — Baeldung](https://www.baeldung.com/spring-async) * [Spring Events — Baeldung](https://www.baeldung.com/spring-events) * [Getting Started: Messaging with JMS — Spring Guides](https://spring.io/guides/gs/messaging-jms/) * [Apache ActiveMQ](https://activemq.apache.org/) * [Spring Boot Starter Mail — Baeldung](https://www.baeldung.com/spring-email) * [WebSocket with Spring Boot — Baeldung](https://www.baeldung.com/spring-websockets-sendtouser) * [Scheduling Tasks — Spring Boot Reference](https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html) --- ## ლაბორატორიული დავალება (Lab Assignment): Asynchronous Messaging-ის იმპლემენტაცია დაამატეთ ასინქრონული დამუშავება და messaging თქვენს აპლიკაციაში. **მოთხოვნები (Requirements):** 1. **დააიმპლემენტირეთ მინიმუმ ერთი `@Async` მეთოდი** — მაგალითად, ელფოსტის გაგზავნა ან ფაილის ატვირთვის დამუშავება ფონურ რეჟიმში. დააკონფიგურირეთ `@EnableAsync` და დარწმუნდით, რომ მეთოდი ეშვება განცალკევებულ Thread-ზე, Thread-ის სახელის ლოგირების (logging) მეშვეობით. 2. **დააკონფიგურირეთ message broker** ჩამოთვლილთაგან ერთ-ერთის გამოყენებით: ActiveMQ Classic (ჩაშენებული (embedded) ან standalone), ActiveMQ Artemis, RabbitMQ ან Kafka. დააიმპლემენტირეთ მინიმუმ ერთი პროდიუსერი (producer), რომელიც აგზავნის შეტყობინებებს და ერთი კონსიუმერი (consumer), რომელიც იღებს და ამუშავებს მათ. 3. **გამოიყენეთ JSON სერიალიზაცია (serialization)** შეტყობინებებისთვის, ნაცვლად Java სერიალიზაციისა. დააკონფიგურირეთ `MappingJackson2MessageConverter` (JMS-ისთვის) ან მისი ეკვივალენტი თქვენ მიერ არჩეული ბროკერისთვის. 4. **დაამატეთ ლოგირება (logging)**, რათა აჩვენოთ, რომ შეტყობინებები იგზავნება და მიიღება ასინქრონულად — დროის შტამპები (timestamps) და Thread-ების სახელები უნდა აჩვენებდეს, რომ პროდიუსერი არ ელოდება კონსიუმერს. **ბონუს გამოწვევები (Bonus challenges):** 5. დაამატეთ **დაგეგმილი ამოცანა (scheduled task)** `@Scheduled`-ით, რომელიც პერიოდულად გააგზავნის შეტყობინებებს რიგში (queue) (მაგ. heartbeat ან მონაცემთა პერიოდული სინქრონიზაცია (sync)). 6. გააგზავნეთ **ელფოსტის ნოტიფიკაცია** შეტყობინების მიღებისას, `JavaMailSender`-ის და FreeMarker HTML თემფლეითის გამოყენებით. 7. დააიმპლემენტირეთ **WebSocket endpoint-ი**, რომელიც აბროუდქასთებს (broadcasts) მიღებულ შეტყობინებებს დაკავშირებული ბრაუზერის კლიენტებისთვის რეალურ დროში.