# თავი 2: Spring-ის ძირითადი კონცეფციები და Logging --- ## 2.1 ApplicationContext პირველ თავში ჩვენ შევქმენით Spring Boot აპლიკაცია, გავუშვით ის და ვნახეთ, თუ როგორ ემსახურება ვებგვერდებს. ამ ერთი შეხედვით მარტივი ქმედების უკან რთული მექანიზმი მუშაობდა. ამ მექანიზმის ცენტრში დგას **ApplicationContext** — ყველა Spring აპლიკაციის გული. ApplicationContext არის Spring-ის **IoC container**. ის არის ობიექტი, რომელიც ქმნის, აკონფიგურირებს, ერთმანეთთან აკავშირებს და მართავს აპლიკაციაში არსებული ყველა სხვა ობიექტის სასიცოცხლო ციკლს. როდესაც თქვენს `main` მეთოდში იძახებთ `SpringApplication.run(BlogApplication.class, args)`-ს, Spring Boot აშენებს ApplicationContext-ს. ამ მომენტიდან კონტეინერი იღებს კონტროლს. როდესაც აპლიკაცია ირთვება, ApplicationContext ასრულებს შემდეგ მოქმედებებს: 1. ის **ასკანერებს (scans)** თქვენი პროექტის პაკეტებს კლასებზე, რომლებიც ანოტირებულია Spring სტერეოტიპებით — `@Component`, `@Service`, `@Repository`, `@Controller` და სხვა. 2. ის **ახდენს ამ კლასების ინსტანცირებას (instantiates)**, ქმნის ობიექტებს, რომლებიც ცნობილია როგორც **bean**-ები. 3. ის **წყვეტს და აინექცირებს (resolves and injects)** დამოკიდებულებებს bean-ებს შორის — თუ ერთ bean-ს სჭირდება მეორე, კონტეინერი უზრუნველყოფს მას. 4. ის ხდის ყველა bean-ს **გამოსაყენებლად ხელმისაწვდომს** მთელი აპლიკაციის მასშტაბით. ტერმინი "bean" უბრალოდ Spring-ის სიტყვაა ობიექტისთვის, რომელსაც მართავს კონტეინერი. bean არ არის რაიმე განსაკუთრებული სახის Java ობიექტი — ის არის ჩვეულებრივი ობიექტი, რომლის შექმნასა და სასიცოცხლო ციკლს აკონტროლებს Spring და არა უშუალოდ თქვენი კოდი. --- ## 2.2 Inversion of Control პრინციპი, რომელიც უდევს საფუძვლად მთელ Spring Framework-ს, არის **Inversion of Control (IoC)** ანუ კონტროლის ინვერსია. ტრადიციულ პროგრამირებაში, აპლიკაციის კოდი თავად არის პასუხისმგებელი ყველაფერზე. თქვენ წერთ `main` მეთოდს, ქმნით ობიექტებს `new` ოპერატორით და იძახებთ მეთოდებს იმ თანმიმდევრობით, რომელსაც თავად გადაწყვეტთ. აპლიკაცია აკონტროლებს საკუთარ მიმდინარეობას. Inversion of Control-ის შემთხვევაში, ეს ურთიერთობა შებრუნებულია. ფრეიმვორქი იღებს პასუხისმგებლობას ობიექტების შექმნასა და მათი ურთიერთქმედებების მართვაზე. თქვენი კოდი განსაზღვრავს, თუ რა ობიექტები არსებობს და რა სჭირდებათ მათ, მაგრამ ფრეიმვორქი წყვეტს, როდის შექმნას ისინი, როგორ დააკავშიროს ერთმანეთთან და როდის გაანადგუროს. განვიხილოთ კონკრეტული მაგალითი. დავუშვათ, გაქვთ `OrderService`, რომელსაც სჭირდება `OrderRepository` მონაცემთა ბაზასთან წვდომისთვის. ტრადიციულ კოდში, თქვენ შეიძლება დაგეწერათ: ```java public class OrderService { private final OrderRepository orderRepository = new OrderRepository(); } ``` აქ, `OrderService` თავად აკონტროლებს პროცესს — ის ქმნის საკუთარ დამოკიდებულებას. ეს იწვევს მჭიდრო შეჭიდულობას (tight coupling): `OrderService` სამუდამოდ არის მიბმული `OrderRepository`-ის ამ კონკრეტულ იმპლემენტაციაზე. ტესტირება რთულდება. იმპლემენტაციების შეცვლა რთულდება. ცვლილებები აისახება მთელ კოდის ბაზაზე. IoC-ის გამოყენებით, `OrderService` არ ქმნის repository-ს. ამის ნაცვლად, ის აცხადებს, რომ სჭირდება იგი და კონტეინერი აწვდის მას: ```java @Service public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } } ``` კონტეინერი ხედავს, რომ `OrderService`-ს ესაჭიროება `OrderRepository`, პოულობს (ან ქმნის) შესაბამის bean-ს და გადასცემს მას. `OrderService` არასოდეს იძახებს `new OrderRepository()`-ს. მან არ იცის და არც აინტერესებს, როგორ შეიქმნა repository ან რომელი კონკრეტული იმპლემენტაციაა იგი. კონტროლი შებრუნებულია — კონტეინერი მართავს პროცესს. --- ## 2.3 Dependency Injection **Dependency Injection (DI)** არის სპეციფიკური მექანიზმი, რომლის საშუალებითაც ხორციელდება IoC Spring-ში. ნაცვლად იმისა, რომ ობიექტმა თავად შექმნას ან მოძებნოს ის ობიექტები, რომლებზეც არის დამოკიდებული, ეს დამოკიდებულებები *ინექცირდება* — მიეწოდება მას გარედან, კონტეინერის მიერ. Spring მხარს უჭერს დამოკიდებულებების ინექციის სამ სტილს. ### Constructor Injection (რეკომენდებული) ```java @Service public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } } ``` დამოკიდებულება მიეწოდება კონსტრუქტორის მეშვეობით. ველი შეიძლება გამოცხადდეს როგორც `final`, რაც გარანტიას იძლევა, რომ ის დაყენდება ზუსტად ერთხელ და მოგვიანებით მისი შეცვლა შეუძლებელია. როდესაც კლასს აქვს მხოლოდ ერთი კონსტრუქტორი, Spring იყენებს მას ავტომატურად — ანოტაცია არ არის საჭირო. ### Setter Injection ```java @Service public class OrderService { private OrderRepository orderRepository; @Autowired public void setOrderRepository(OrderRepository orderRepository) { this.orderRepository = orderRepository; } } ``` დამოკიდებულება მიეწოდება setter მეთოდის მეშვეობით, რომელიც ანოტირებულია `@Autowired`-ით. ეს სტილი შესაფერისია არასავალდებულო (optional) დამოკიდებულებებისთვის, რომლებიც შეიძლება იყოს ან არ იყოს წარმოდგენილი. ### Field Injection (არ არის რეკომენდებული) ```java @Service public class OrderService { @Autowired private OrderRepository orderRepository; } ``` დამოკიდებულება ინექცირდება პირდაპირ ველში. მიუხედავად იმისა, რომ ეს მიდგომა ლაკონურია, მას აქვს მნიშვნელოვანი ნაკლოვანებები: ის მალავს დამოკიდებულებებს კლასის საჯარო API-დან, ხელს უშლის `final`-ის გამოყენებას და ართულებს unit ტესტირებას, რადგან თქვენ არ შეგიძლიათ მარტივად მიაწოდოთ ტესტის ორეულები (test doubles) კონსტრუქტორის მეშვეობით. **Constructor injection არის რეკომენდებული მიდგომა** მთელი ამ კურსის განმავლობაში. ის დამოკიდებულებებს ხდის ექსპლიციტურს, აიძულებს უცვლელობას (immutability) და ქმნის კლასებს, რომელთა ტესტირებაც მარტივია. --- ## 2.4 დამოკიდებულების ინექციის ამბივალენტურობის გადაჭრა როდესაც Spring აინექცირებს დამოკიდებულებას, ის ეძებს ApplicationContext-ში ისეთ bean-ს, რომელიც შეესაბამება მოთხოვნილ ტიპს. უმეტეს შემთხვევაში, საჭირო ტიპის მხოლოდ ერთი bean არსებობს და ინექცია მუშაობს ყოველგვარი დამატებითი მითითების გარეშე. მაგრამ რა ხდება მაშინ, როდესაც ერთი და იმავე ტიპის ორი ან მეტი bean არსებობს? ### პრობლემა: ერთი და იმავე ტიპის რამდენიმე Bean დავუშვათ, გვაქვს `NotificationSender` ინტერფეისი და მისი ორი იმპლემენტაცია: ```java public interface NotificationSender { void send(String message); } @Component public class EmailNotificationSender implements NotificationSender { @Override public void send(String message) { // sends via email } } @Component public class SmsNotificationSender implements NotificationSender { @Override public void send(String message) { // sends via SMS } } ``` ორივე კლასი ანოტირებულია `@Component`-ით, ამიტომ Spring ორივეს არეგისტრირებს როგორც bean-ებს. ახლა განვიხილოთ სერვისი, რომელიც დამოკიდებულია `NotificationSender`-ზე: ```java @Service public class OrderService { private final NotificationSender notificationSender; public OrderService(NotificationSender notificationSender) { this.notificationSender = notificationSender; } } ``` როდესაც Spring შეეცდება `NotificationSender`-ის ინექციას `OrderService`-ში, ის იპოვის ორ კანდიდატს: `EmailNotificationSender` და `SmsNotificationSender`. მას არ შეუძლია გადაწყვიტოს რომელი გამოიყენოს, ამიტომ აპლიკაციის ჩართვისას ის დააფიქსირებს შეცდომას: ``` No qualifying bean of type 'NotificationSender' available: expected single matching bean but found 2: emailNotificationSender, smsNotificationSender ``` ეს ჩვეულებრივი სიტუაციაა რეალურ აპლიკაციებში — შეიძლება გქონდეთ მონაცემთა ბაზის მრავალი კონფიგურაცია, სტრატეგიის ინტერფეისის მრავალი იმპლემენტაცია ან მესამე მხარის (third-party) სერვისის მრავალი კლიენტი. Spring გვთავაზობს რამდენიმე მექანიზმს ამ ამბივალენტურობის გადასაჭრელად. ### გადაწყვეტა 1: `@Primary` `@Primary` ანოტაცია აღნიშნავს ერთ-ერთ bean-ს როგორც ნაგულისხმევ არჩევანს, როდესაც მრავალი კანდიდატი არსებობს: ```java @Primary @Component public class EmailNotificationSender implements NotificationSender { @Override public void send(String message) { // sends via email } } @Component public class SmsNotificationSender implements NotificationSender { @Override public void send(String message) { // sends via SMS } } ``` ახლა, როდესაც Spring-ს დასჭირდება `NotificationSender` და დამატებითი კვალიფიკაცია არ იქნება მითითებული, ის დააინექცირებს `EmailNotificationSender`-ს. `SmsNotificationSender` კვლავ ვალიდური bean-ია და საჭიროების შემთხვევაში მაინც შეიძლება მისი ექსპლიციტურად ინექცირება. `@Primary` სწორი არჩევანია, როდესაც ერთი იმპლემენტაცია არის "ჩვეულებრივი" ან "ნაგულისხმევი", ხოლო მეორე გამოიყენება მხოლოდ სპეციფიკურ გარემოებებში. ### გადაწყვეტა 2: `@Qualifier` `@Qualifier` ანოტაცია საშუალებას გაძლევთ ზუსტად მიუთითოთ, რომელი bean გსურთ მისი სახელის მიხედვით: ```java @Service public class OrderService { private final NotificationSender notificationSender; public OrderService(@Qualifier("smsNotificationSender") NotificationSender notificationSender) { this.notificationSender = notificationSender; } } ``` სტრიქონი `"smsNotificationSender"` არის bean-ის სახელი — ნაგულისხმევად, Spring ქმნის bean-ის სახელს კლასის სახელიდან პატარა საწყისი ასოთი. `@Qualifier` ანოტაცია ეუბნება Spring-ს, უგულებელყოს ნებისმიერი `@Primary` აღნიშვნა და დააინექციროს კონკრეტულად დასახელებული bean. `@Qualifier` სწორი არჩევანია, როდესაც ერთი და იმავე ინტერფეისის სხვადასხვა მომხმარებელს ესაჭიროება განსხვავებული იმპლემენტაციები. მაგალითად, ერთ სერვისს შეიძლება დასჭირდეს email გამგზავნი, ხოლო მეორეს — SMS გამგზავნი. ### გადაწყვეტა 3: ინექცია ველის სახელის მიხედვით Spring ასევე ითვალისწინებს პარამეტრის სახელს, როგორც მინიშნებას. თუ კონსტრუქტორის პარამეტრის სახელი ემთხვევა bean-ის სახელს, Spring გამოიყენებს ამ დამთხვევას ამბივალენტურობის გადასაჭრელად: ```java @Service public class OrderService { private final NotificationSender smsNotificationSender; public OrderService(NotificationSender smsNotificationSender) { this.smsNotificationSender = smsNotificationSender; } } ``` აქ, პარამეტრს ჰქვია `smsNotificationSender`, რაც ემთხვევა SMS იმპლემენტაციის bean-ის სახელს. Spring ამბივალენტურობას შესაბამისად წყვეტს. მიუხედავად იმისა, რომ ეს მუშაობს, ის ნაკლებად ექსპლიციტურია ვიდრე `@Qualifier` და შეიძლება მყიფე აღმოჩნდეს — პარამეტრის სახელის გადარქმევამ შეიძლება უხმოდ დაარღვიოს ინექცია. სიცხადისთვის, უპირატესობა მიანიჭეთ `@Qualifier`-ს, როდესაც განზრახვა აშკარა უნდა იყოს. ### ყველა იმპლემენტაციის ინექცია ზოგჯერ თქვენ გსურთ მოცემული ტიპის ყველა bean — მაგალითად, შეტყობინების გასაგზავნად ყველა ხელმისაწვდომი არხით. თქვენ შეგიძლიათ დააინექციროთ ინტერფეისის ტიპის `List`: ```java @Service public class BroadcastService { private final List senders; public BroadcastService(List senders) { this.senders = senders; } public void broadcast(String message) { senders.forEach(sender -> sender.send(message)); } } ``` Spring ავტომატურად აგროვებს ყველა bean-ს, რომელიც აიმპლემენტირებს `NotificationSender`-ს და აინექცირებს მათ როგორც სიას. ეს პატერნი სასარგებლოა plugin არქიტექტურებისთვის, ივენთების (event) დამმუშავებლებისთვის ან ნებისმიერ სიტუაციაში, სადაც გსურთ გაფართოებადობა (extensibility) არსებული კოდის მოდიფიკაციის გარეშე. --- ## 2.5 Bean-ის სასიცოცხლო ციკლი ყველა Spring bean გადის კარგად განსაზღვრულ სასიცოცხლო ციკლს, რომელსაც მთლიანად მართავს ApplicationContext. **ინსტანცირება (Instantiation).** კონტეინერი ქმნის bean ობიექტს — როგორც წესი, მისი კონსტრუქტორის გამოძახებით. **დამოკიდებულებების ინექცია (Dependency Injection).** კონტეინერი წყვეტს და აინექცირებს ყველა დამოკიდებულებას, რომელიც მოითხოვება bean-ის მიერ. **ინიციალიზაცია (Initialization).** თუ bean განსაზღვრავს რაიმე საინიციალიზაციო ლოგიკას, ის სრულდება ამ ეტაპზე. საინიციალიზაციო ლოგიკის განსაზღვრის ყველაზე გავრცელებული გზაა `@PostConstruct` ანოტაცია. **მზადაა გამოსაყენებლად (Ready for Use).** bean ახლა სრულად ინიციალიზებულია და ხელმისაწვდომია სხვა bean-ებისა და აპლიკაციის მიერ გამოსაყენებლად. **განადგურება (Destruction).** როდესაც ApplicationContext ითიშება (მაგ., როდესაც აპლიკაცია ჩერდება), კონტეინერი იძახებს განადგურების callback-ებს. `@PreDestroy` ანოტაცია აღნიშნავს მეთოდს, რომელიც უნდა გამოიძახოს ამ ეტაპზე. ```java @Component public class CacheManager { @PostConstruct public void init() { // Called after all dependencies have been injected. // Use this for setup logic: loading caches, opening connections, etc. } @PreDestroy public void shutdown() { // Called when the application context is shutting down. // Use this for cleanup: closing connections, flushing buffers, etc. } } ``` სასიცოცხლო ციკლის გაგებას აქვს მნიშვნელობა, რადგან ის გეუბნებათ სად უნდა მოათავსოთ setup და teardown ლოგიკა. ინიციალიზაციის კოდი ეკუთვნის `@PostConstruct` მეთოდს — და არა კონსტრუქტორს, რადგან კონსტრუქტორი ეშვება დამოკიდებულებების ინექციამდე (setter ან field ინექციის შემთხვევაში), და არა შემთხვევით ბიზნეს მეთოდში, რომელიც შეიძლება გამოიძახოს ან არა. --- ## 2.6 Bean-ის Scope-ები როდესაც ApplicationContext ქმნის bean-ს, რამდენ ინსტანსს ქმნის ის და რამდენ ხანს ცოცხლობენ ისინი? პასუხი დამოკიდებულია bean-ის **scope**-ზე. ნაგულისხმევად, ყველა Spring bean არის **singleton** — კონტეინერი ქმნის ზუსტად ერთ ინსტანსს და ყველა კომპონენტი, რომელიც ითხოვს bean-ს, იღებს ერთსა და იმავე ობიექტს. ვებ აპლიკაციების bean-ების აბსოლუტური უმრავლესობისთვის (სერვისები, repository-ები, controller-ები, კონფიგურაციის კლასები), ეს არის სწორი ქცევა. ეს ობიექტები არიან stateless ან thread-safe, და ერთი ინსტანსის გაზიარება არის უსაფრთხოც და ეფექტურიც. თუმცა, არსებობს სიტუაციები, როდესაც გჭირდებათ განსხვავებული სასიცოცხლო ციკლი: | Scope | ქცევა (Behavior) | | --- | --- | | `singleton` | თითო ინსტანსი ApplicationContext-ზე. ეს არის ნაგულისხმევი. | | `prototype` | ახალი ინსტანსი იქმნება ყოველ ჯერზე, როდესაც bean მოითხოვება. | | `request` | თითო ინსტანსი HTTP მოთხოვნაზე (მხოლოდ ვებ აპლიკაციებისთვის). | | `session` | თითო ინსტანსი HTTP სესიაზე (მხოლოდ ვებ აპლიკაციებისთვის). | თქვენ აცხადებთ არა-ნაგულისხმევ scope-ს `@Scope` ანოტაციით: ```java @Component @Scope("prototype") public class ShoppingCart { private List items = new ArrayList<>(); // A new ShoppingCart is created each time it is requested. } ``` გაფრთხილება: prototype-scope-ის მქონე bean-ის ინექცია singleton-scope-ის მქონე bean-ში არ იძლევა იმ ქცევას, რასაც შეიძლება ელოდოთ. singleton იქმნება ერთხელ, ამიტომ მისი დამოკიდებულებები ინექცირდება ერთხელ. prototype bean, რომელსაც ის იღებს, იქნება ერთი და იგივე ინსტანსი singleton-ის მთელი სიცოცხლის განმავლობაში. თუ ნამდვილად გჭირდებათ ახალი prototype ინსტანსი ყოველი გამოყენებისას, მოგიწევთ შეისწავლოთ `ObjectProvider` ან method injection — თემები, რომლებსაც კურსის განმავლობაში მოგვიანებით დავუბრუნდებით. --- ## 2.7 Component Scanning და სტერეოტიპული ანოტაციები ზემოთ მოყვანილ მაგალითებში, ჩვენ კლასებს ვაანოტირებდით `@Service`, `@Component` და `@Controller` ანოტაციებით. ეს არის **სტერეოტიპული ანოტაციები** და ისინი ემსახურებიან ორ მიზანს: ისინი აღნიშნავენ კლასს, როგორც Spring-ის მიერ მართულ bean-ს (რათა კონტეინერმა აღმოაჩინოს და მოახდინოს მისი ინსტანცირება), და ისინი გამოხატავენ კლასის როლს აპლიკაციის არქიტექტურაში. | ანოტაცია | როლი | | --- | --- | | `@Component` | ზოგადი Spring-ის მიერ მართული bean. გამოიყენება მაშინ, როდესაც არცერთი უფრო სპეციფიკური სტერეოტიპი არ შეესაბამება. | | `@Service` | bean, რომელიც შეიცავს ბიზნეს ლოგიკას — სერვისის ფენა. | | `@Repository` | bean, რომელიც მართავს მონაცემებთან წვდომას — პერსისტენტობის ფენა. Spring ასევე იყენებს ავტომატურ გამონაკლისების თარგმნას (exception translation) `@Repository` bean-ებისთვის, გადაყავს რა მონაცემთა ბაზის სპეციფიკური გამონაკლისები Spring-ის `DataAccessException` იერარქიაში. | | `@Controller` | ვებ controller-ი, რომელიც ამუშავებს HTTP მოთხოვნებს და აბრუნებს view-ების სახელებს (შაბლონებს). | | `@RestController` | REST controller-ი, რომელიც აბრუნებს მონაცემებს პირდაპირ (JSON, XML და ა.შ.). ის აერთიანებს `@Controller`-ს `@ResponseBody`-სთან. | ყველა ეს ანოტაცია არის `@Component`-ის სპეციალიზაცია. კონტეინერის პერსპექტივიდან, ისინი ყველა იწვევენ კლასის დარეგისტრირებას bean-ად. განსხვავება ძირითადად ორგანიზაციულია — ის ეხმარება დეველოპერებს (და ზოგ შემთხვევაში თავად Spring-ს) გაიგონ თითოეული კლასის დანიშნულება. Spring Boot პოულობს ამ ანოტირებულ კლასებს **component scanning**-ის (კომპონენტების სკანირების) მეშვეობით. ნაგულისხმევად, სკანირება იწყება იმ პაკეტიდან, სადაც მდებარეობს `@SpringBootApplication` კლასი და მოიცავს ყველა ქვე-პაკეტს. სწორედ ამიტომ არის მნიშვნელოვანი თქვენი მთავარი აპლიკაციის კლასის მოთავსება თქვენი პაკეტების იერარქიის ძირში (root): ``` ge.tsu.blog ├── BlogApplication.java ← @SpringBootApplication lives here ├── controller/ │ └── PostController.java ← found by component scanning ├── service/ │ └── PostService.java ← found by component scanning └── repository/ └── PostRepository.java ← found by component scanning ``` თუ კლასი მოთავსებულია ამ პაკეტის ხის გარეთ, ის ვერ მოიძებნება მანამ, სანამ არ დააკონფიგურირებთ დამატებითი სკანირების ლოკაციებს. --- ## 2.8 Bean-ების ექსპლიციტურად განსაზღვრა `@Bean`-ით სტერეოტიპული ანოტაციები კარგად მუშაობს იმ კლასებისთვის, რომლებსაც თავად წერთ. მაგრამ რა ხდება მესამე მხარის (third-party) ბიბლიოთეკების კლასებთან მიმართებაში? თქვენ არ შეგიძლიათ დაამატოთ `@Service` იმ კლასს, რომელიც არ დაგიწერიათ. ასეთ შემთხვევებში, თქვენ bean-ებს ექსპლიციტურად საზღვრავთ `@Bean` ანოტაციის გამოყენებით `@Configuration` კლასის შიგნით. ```java @Configuration public class AppConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper; } } ``` თითოეული მეთოდი, რომელიც ანოტირებულია `@Bean`-ით, აწარმოებს bean-ს. მეთოდის სახელი ხდება bean-ის სახელი ნაგულისხმევად (`restTemplate`, `objectMapper`). დასაბრუნებელი (return) ტიპი განსაზღვრავს bean-ის ტიპს. კონტეინერი იძახებს ამ მეთოდებს და არეგისტრირებს დაბრუნებულ ობიექტებს როგორც bean-ებს ApplicationContext-ში. `@Configuration` ანოტაცია ეუბნება Spring-ს, რომ ეს კლასი შეიცავს bean-ების განმარტებებს და ის უნდა დამუშავდეს გაშვებისას. მის გარეშე, `@Bean` მეთოდები იგნორირებული იქნება. ეს მიდგომა ასევე სასარგებლოა მაშინ, როდესაც გჭირდებათ მორგებული საინიციალიზაციო ლოგიკა, რომელიც სცდება მარტივი კონსტრუქტორის შესაძლებლობებს — მაგალითად, connection pool-ის კონფიგურაცია, HTTP კლიენტის დაყენება კონკრეტული timeout-ებით ან მორგებული serializer-ების რეგისტრაცია. --- ## 2.9 Auto-Configuration ხელახლა პირველ თავში ჩვენ ვახსენეთ, რომ Spring Boot ავტომატურად აკონფიგურირებს თქვენს აპლიკაციას classpath-ზე არსებული ბიბლიოთეკების საფუძველზე. ახლა, როდესაც გვესმის bean-ები, ApplicationContext და `@Configuration` კლასები, შეგვიძლია უფრო ზუსტად განვიხილოთ ეს მექანიზმი. Auto-configuration თავისი არსით არის იმ `@Configuration` კლასების კოლექცია, რომელიც მოყვება Spring Boot-ს. თითოეული კლასი პასუხისმგებელია ერთი კონკრეტული ფუნქციის კონფიგურაციაზე. მაგალითად, არსებობს auto-configuration კლასი FreeMarker-ისთვის, სხვა ჩაშენებული Tomcat სერვერისთვის, კიდევ სხვა მონაცემთა წყაროებისთვის (data sources) და ა.შ. რაც ამ კლასებს განსაკუთრებულს ხდის, არის ის, რომ ისინი **პირობითია (conditional)**. თითოეული მათგანი ანოტირებულია პირობებით, რომლებიც განსაზღვრავს, უნდა გააქტიურდეს თუ არა ის: * `@ConditionalOnClass` — გააქტიურდეს მხოლოდ იმ შემთხვევაში, თუ კონკრეტული კლასი არსებობს classpath-ზე. * `@ConditionalOnMissingBean` — გააქტიურდეს მხოლოდ იმ შემთხვევაში, თუ დეველოპერს უკვე არ განუსაზღვრავს ამ ტიპის bean. * `@ConditionalOnProperty` — გააქტიურდეს მხოლოდ იმ შემთხვევაში, თუ კონკრეტული კონფიგურაციის property დაყენებულია. სწორედ ასე რჩება Spring Boot უხილავი თქვენთვის. თუ თქვენ განსაზღვრავთ საკუთარ `FreeMarkerConfigurer` bean-ს, FreeMarker-ის auto-configuration აღმოაჩენს მას (მეშვეობით `@ConditionalOnMissingBean`) და უკან იხევს, იყენებს რა თქვენს bean-ს თავისი ნაგულისხმევის ნაცვლად. იმის სანახავად, ზუსტად რომელი auto-configuration გააქტიურდა და რომელი გამოტოვდა, დაამატეთ ეს `application.properties`-ში: ```properties debug=true ``` აპლიკაცია გაშვებისას დაბეჭდავს პირობების შეფასების დეტალურ რეპორტს — შეუფასებელი ინსტრუმენტი, როდესაც გჭირდებათ გაიგოთ, რატომ კონფიგურირდება რაღაც (ან არ კონფიგურირდება). --- ## 2.10 მონაცემების გადაცემა Controller-ებიდან შაბლონებში პირველ თავში, ჩვენ ვიყენებდით view controller-ებს URL-ების პირდაპირ FreeMarker შაბლონებთან დასაკავშირებლად. View controller-ები მოსახერხებელია სტატიკური გვერდებისთვის, მაგრამ მათ არ შეუძლიათ მონაცემების გადაცემა შაბლონისთვის. დინამიური გვერდებისთვის — გვერდებისთვის, რომლებიც აჩვენებენ სერვისიდან, მონაცემთა ბაზიდან ან ნებისმიერი სხვა წყაროდან ამოღებულ მონაცემებს — ჩვენ გვჭირდება სათანადო `@Controller` კლასი. აი ძირითადი პატერნი: ```java @Controller public class PostController { private final PostService postService; public PostController(PostService postService) { this.postService = postService; } @GetMapping("/posts") public String listPosts(Model model) { List posts = postService.findAll(); model.addAttribute("posts", posts); model.addAttribute("pageTitle", "All Posts"); return "posts"; // resolves to templates/posts.ftlh } } ``` აქ რამდენიმე რამ ხდება: კლასი ანოტირებულია `@Controller`-ით, რაც მას აღნიშნავს როგორც ვებ controller-ს და არეგისტრირებს მას როგორც bean-ს. მისი დამოკიდებულება — `PostService` — ინექცირებულია კონსტრუქტორის მეშვეობით. `listPosts` მეთოდი დაკავშირებულია (mapped) `GET /posts`-თან `@GetMapping`-ის მეშვეობით. ის იღებს `Model` ობიექტს, რომელიც არის კონტეინერი იმ მონაცემებისთვის, რომლებიც გაიგზავნება შაბლონში. მეთოდი ამატებს ორ ატრიბუტს model-ს: პოსტების სიას და გვერდის სათაურს. შემდეგ ის აბრუნებს სტრიქონს `"posts"`, რომელსაც Spring უკავშირებს შაბლონის ფაილს `templates/posts.ftlh`. FreeMarker შაბლონის შიგნით, ეს model-ის ატრიბუტები ხელმისაწვდომი ხდება როგორც ცვლადები: ```html <#import "layout.ftlh" as layout> <@layout.page title=pageTitle>

${pageTitle}

<#list posts as post> ``` `${pageTitle}` ინტერპოლაცია გამოიტანს იმ სტრიქონს, რომელიც ჩვენ მოვათავსეთ model-ში. `<#list posts as post>` დირექტივა გადაუყვება სიას. ციკლის შიგნით, `${post.title}` და `${post.summary}` წვდება თითოეული `Post` ობიექტის property-ებს. ეს არის Spring MVC აპლიკაციის მონაცემთა ფუნდამენტური ნაკადი: controller ამოიღებს მონაცემებს, ათავსებს მას model-ში და ასახელებს შაბლონს. FreeMarker იღებს model-ს და არენდერებს საბოლოო HTML-ს. controller არასოდეს ეხება HTML-ს; შაბლონი არასოდეს ეხება ბიზნეს ლოგიკას. გამიჯვნა არის სუფთა და მიზანმიმართული. --- ## 2.11 შესავალი Logging-ში (ლოგირებაში) აქამდე, როდესაც რაღაც არასწორად მიდიოდა ან გვინდოდა გვენახა, რას აკეთებდა ჩვენი აპლიკაცია, შეიძლება გაგვჩენოდა `System.out.println()`-ის გამოყენების სურვილი. ეს მუშაობს ტრივიალურ პროგრამაში, მაგრამ სრულიად არაადეკვატურია რეალური აპლიკაციისთვის. ბეჭდვის ბრძანებების ჩართვა ან გამორთვა შეუძლებელია კოდის შეცვლის გარეშე. მათი გადამისამართება სხვადასხვა დანიშნულების ადგილზე შეუძლებელია. მათ არ აქვთ მეტამონაცემები — არც დროის ნიშნულები, არც სიმკაცრის (severity) დონეები, არც კლასების სახელები. **Logging (ლოგირება)** არის დისციპლინირებული ალტერნატივა. ეს არის აპლიკაციის მუშაობის დროს მომხდარი მოვლენების — მეთოდების გამოძახებების, მიღებული გადაწყვეტილებების, დაფიქსირებული შეცდომების, წარმადობის გაზომვების — ჩაწერის პრაქტიკა სტრუქტურირებული, კონფიგურირებადი და არა-ინტრუზიული გზით. სათანადო logging სისტემა გაძლევთ: **კონფიგურირებადობას.** შეგიძლიათ აკონტროლოთ, რომელი შეტყობინებები ჩაიწეროს და რომელი დაიმალოს, აპლიკაციის კოდის არცერთი ხაზის შეცვლის გარეშე. დეველოპმენტის დროს შეიძლება გსურდეთ ყოველი დეტალის ნახვა. production-ში შეიძლება გსურდეთ მხოლოდ გაფრთხილებები და შეცდომები. **მრავალ დანიშნულების ადგილს.** ლოგის შეტყობინებები შეიძლება ჩაიწეროს კონსოლში, ფაილებში, დაშორებულ logging სერვერზე ან ყველა მათგანზე ერთდროულად. **სტრუქტურას.** ყოველ ლოგის შეტყობინებას ახლავს მეტამონაცემები: დროის ნიშნული, სიმკაცრის დონე, კლასი, რომელმაც ის აწარმოა, თრედი (thread), რომელზეც ის გაეშვა. ეს ხდის ლოგებს საძიებოსა და ანალიზებადს. **წარმადობას.** თანამედროვე logging ფრეიმვორქები შექმნილია ისე, რომ ჰქონდეს მინიმალური დანახარჯები, როდესაც ლოგის დონე გამორთულია, ასე რომ თქვენ შეგიძლიათ დატოვოთ `debug` ჩანაწერები თქვენს კოდში ისე, რომ არ გადაიხადოთ წარმადობის ჯარიმა production-ში. --- ## 2.12 ნაგულისხმევი Logging Spring Boot-ში Java-ს ეკოსისტემას აქვს logging ბიბლიოთეკების ხანგრძლივი ისტორია: `java.util.logging` (ჩაშენებული JDK-ში), Apache Log4j, Logback და რამდენიმე სხვა. იმისათვის, რომ თავიდან იქნას აცილებული აპლიკაციის კოდის მიბმა კონკრეტულ იმპლემენტაციაზე, შეიქმნა აბსტრაქციის ფენა სახელად **SLF4J** (Simple Logging Facade for Java). SLF4J განსაზღვრავს საერთო API-ს — თქვენ წერთ თქვენს logging გამოძახებებს SLF4J-ის მიმართ, ხოლო რეალურ logging სამუშაოს ასრულებს ის იმპლემენტაცია, რომელიც წარმოდგენილია გაშვების დროს. Spring Boot იყენებს ამ არქიტექტურას თავიდანვე: * **API**, რომლის მიმართაც თქვენ აპროგრამებთ არის **SLF4J**. * **იმპლემენტაცია**, რომელიც რეალურ საქმეს აკეთებს არის **Logback** (რომელიც შეიქმნა იმავე დეველოპერის მიერ, რომელმაც დაწერა SLF4J). * ნაგულისხმევად, ლოგის შეტყობინებები იწერება **კონსოლში**. ამისთვის დამოკიდებულებების დამატება არ გჭირდებათ — `spring-boot-starter-web` (და სხვა სტარტერების უმეტესობა) მოიცავს `spring-boot-starter-logging`-ს, რომელსაც ავტომატურად შემოაქვს SLF4J და Logback. --- ## 2.13 Log-ის დონეები ყოველ ლოგის შეტყობინებას აქვს **დონე**, რომელიც მიუთითებს მის სიმკაცრეზე. დონეები, მნიშვნელობის ზრდის მიხედვით, არის: | დონე | დანიშნულება | | --- | --- | | `TRACE` | უკიდურესად დეტალური ინფორმაცია. იშვიათად გამოიყენება ფრეიმვორქის შიდა მექანიზმების მიღმა. | | `DEBUG` | ინფორმაცია, რომელიც სასარგებლოა დეველოპმენტის დროს — ცვლადების მნიშვნელობები, გადაწყვეტილების მიღების წერტილები, ნაკადის დეტალები. | | `INFO` | მნიშვნელოვანი მოვლენები აპლიკაციის სასიცოცხლო ციკლში — ჩართვა, გამორთვა, ძირითადი ოპერაციების დასრულება. | | `WARN` | რაღაც მოულოდნელი მოხდა, მაგრამ აპლიკაციას შეუძლია გაგრძელება. პოტენციური პრობლემა, რომელიც ყურადღებას იმსახურებს. | | `ERROR` | სერიოზული მარცხი. ოპერაციის დასრულება ვერ მოხერხდა. სავარაუდოდ საჭიროა დაუყოვნებელი ყურადღება. | როდესაც აყენებთ ლოგის დონეს, სისტემა იწერს შეტყობინებებს ამ დონეზე **და უფრო ზემოთ**. მაგალითად, თუ დონე დაყენებულია `INFO`-ზე, მაშინ ჩაიწერება `INFO`, `WARN` და `ERROR` შეტყობინებები, ხოლო `DEBUG` და `TRACE` შეტყობინებები დაიმალება. ლოგის დონეების კონფიგურაცია ხდება `application.properties`-ში: ```properties # Set the root (default) level for the entire application logging.level.root=INFO # Set a more detailed level for your own packages logging.level.ge.tsu.blog=DEBUG # Suppress noisy output from a specific library logging.level.org.hibernate.SQL=WARN ``` ეს გრანულარობა ნამდვილი logging სისტემის ერთ-ერთი დიდი ძალაა. თქვენ შეგიძლიათ ნახოთ დეტალური გამომავალი ინფორმაცია თქვენი საკუთარი კოდიდან, ხოლო მესამე მხარის ბიბლიოთეკების ხმაური მინიმუმამდე შეამციროთ. --- ## 2.14 ლოგის შეტყობინებების ჩაწერა ### მექანიკური მიდგომა ნებისმიერ კლასში logger-ის მისაღებად გამოიყენეთ SLF4J-ის `LoggerFactory`: ```java import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Service public class PostService { private static final Logger log = LoggerFactory.getLogger(PostService.class); public List findAll() { log.info("Retrieving all posts"); List posts = postRepository.findAll(); log.debug("Found {} posts", posts.size()); return posts; } public Post findById(Long id) { log.debug("Looking up post with id={}", id); return postRepository.findById(id) .orElseThrow(() -> { log.warn("Post not found: id={}", id); return new PostNotFoundException(id); }); } } ``` ყურადღება მიაქციეთ **პარამეტრიზებული შეტყობინებების** გამოყენებას: `log.debug("Found {} posts", posts.size())`. `{}` placeholder (ადგილსამყოფელი) იცვლება არგუმენტით გაშვების დროს. ეს მნიშვნელოვანია — **არ** გამოიყენოთ სტრიქონების კონკატენაცია (`"Found " + posts.size() + " posts"`), რადგან კონკატენაცია ხდება იმისდა მიუხედავად, ჩართულია თუ არა `DEBUG`. პარამეტრიზებული შეტყობინებების გამოყენებით, სტრიქონი აიგება მხოლოდ იმ შემთხვევაში, თუ შეტყობინება რეალურად ჩაიწერება. ### Lombok-ის მარტივი გზა (Shortcut) რადგან ჩვენს პროექტში გვაქვს Lombok, ჩვენ შეგვიძლია საერთოდ აღმოვფხვრათ logger-ის გამოცხადება `@Slf4j` ანოტაციით: ```java @Slf4j @Service public class PostService { public List findAll() { log.info("Retrieving all posts"); // ... } } ``` Lombok აგენერირებს `private static final Logger log = LoggerFactory.getLogger(PostService.class)` ველს კომპილაციის დროს. შედეგი იდენტურია — ეს არის წმინდად მოხერხებულობა ზედმეტი (boilerplate) კოდის შესამცირებლად. --- ## 2.15 ფაილში ლოგირება კონსოლის გამომავალი ინფორმაცია ქრება, როდესაც აპლიკაცია ჩერდება. ნებისმიერი აპლიკაციისთვის, რომელიც ეშვება სერვერულ გარემოში, მოგინდებათ რომ ლოგები ფაილშიც ჩაიწეროს. Spring Boot ამას ამარტივებს: ```properties logging.file.name=logs/app.log ``` ეს ერთადერთი property ეუბნება Logback-ს, ჩაწეროს ლოგის შეტყობინებები ფაილში სახელად `app.log` `logs/` დირექტორიის შიგნით (რომელიც ავტომატურად შეიქმნება, თუ არ არსებობს). console appender აგრძელებს მუშაობას file appender-ის პარალელურად — თქვენ იღებთ ორივეს. თუ გჭირდებათ მხოლოდ დირექტორიის კონტროლი (და გსურთ Spring Boot-მა თავად აირჩიოს ფაილის სახელი): ```properties logging.file.path=/var/logs/blog ``` --- ## 2.16 Log-ის პატერნები ყოველი ლოგის შეტყობინება ფორმატდება **პატერნის** მიხედვით — სტრიქონი, რომელიც განსაზღვრავს რა ინფორმაცია გამოჩნდეს და რა თანმიმდევრობით. Spring Boot-ის ნაგულისხმევი პატერნი აწარმოებს გამომავალ ინფორმაციას, რომელიც დაახლოებით ასე გამოიყურება: ``` 2025-12-01 14:30:45.123 INFO 12345 --- [main] g.t.blog.service.PostService : Retrieving all posts ``` თქვენ შეგიძლიათ დააკონფიგურიროთ პატერნი კონსოლისთვის და ფაილისთვის დამოუკიდებლად: ```properties logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n ``` ყველაზე გავრცელებული პატერნის ელემენტებია: | ელემენტი | მნიშვნელობა | | --- | --- | | `%d{...}` | დროის ნიშნული მითითებულ ფორმატში. | | `%thread` | თრედის სახელი. | | `%-5level` | ლოგის დონე, მარცხნიდან შევსებული (padded) 5 სიმბოლომდე. | | `%logger{36}` | logger-ის სახელი (ჩვეულებრივ, კლასის სახელი), შემოკლებული 36 სიმბოლომდე. | | `%msg` | ლოგის შეტყობინება. | | `%n` | ახალი ხაზი (newline). | | `%highlight(...)` | იყენებს ფერად კოდირებას ლოგის დონის მიხედვით (მხოლოდ კონსოლში). | --- ## 2.17 Logback კონფიგურაცია `logback-spring.xml`-ით იმ მოთხოვნებისთვის, რომლებიც სცდება `application.properties`-ის შესაძლებლობებს — როგორიცაა მრავალი appender-ი სხვადასხვა დონეებით, გადახვევის (rolling) პოლიტიკები ან პირობითი კონფიგურაცია Spring-ის პროფილების (profiles) მიხედვით — თქვენ შეგიძლიათ უზრუნველყოთ სრული Logback კონფიგურაციის ფაილი. შექმენით ფაილი სახელად `logback-spring.xml` დირექტორიაში `src/main/resources/`: ```xml %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n logs/app.log logs/archive/app-%d{yyyy-MM-dd}-%i.log 100MB 30 10GB %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n ``` `` ელემენტი ზედა ნაწილში აიმპორტებს Spring Boot-ის ნაგულისხმევ Logback პარამეტრებს, რაც გაძლევთ გონივრულ საწყის ქცევას. `` ელემენტები განსაზღვრავს, სად მიდის ლოგის შეტყობინებები. `` file appender-ის შიგნით აკონფიგურირებს **ლოგების როტაციას**: როდესაც ლოგის ფაილი მიაღწევს 100MB-ს, ის არქივდება თარიღით მარკირებული სახელით; 30 დღეზე ძველი არქივები იშლება; ხოლო არქივის ჯამური ზომა შეზღუდულია 10GB-ით. ეს ხელს უშლის ლოგების მიერ დისკის შეუზღუდავი სივრცის დაკავებას. `` ელემენტი აყენებს ნაგულისხმევ დონეს და აბამს ორივე appender-ს. `` ელემენტი გადაფარავს დონეს კონკრეტული პაკეტისთვის. გამოიყენეთ `logback-spring.xml` (`-spring` სუფიქსით) და არა უბრალო `logback.xml`. Spring-ზე მორგებული ვარიანტი მხარს უჭერს Spring პროფილებს და property placeholder-ებს, რასაც უბრალო Logback კონფიგურაცია არ უჭერს მხარს. --- ## 2.18 Logging-ის საუკეთესო პრაქტიკები * **გამოიყენეთ SLF4J და არა კონკრეტული იმპლემენტაცია.** თქვენი აპლიკაციის მთელი კოდი უნდა აიმპორტებდეს `org.slf4j`-დან და არასდროს პირდაპირ `ch.qos.logback`-დან ან `org.apache.log4j`-დან. ეს გაძლევთ თავისუფლებას, შეცვალოთ ქვედა დონის იმპლემენტაცია აპლიკაციის კოდზე შეხების გარეშე. * **არასოდეს გამოიყენოთ `System.out.println()` ლოგირებისთვის.** მისი კონფიგურაცია, გაფილტვრა ან გადამისამართება შეუძლებელია. მის ნაცვლად გამოიყენეთ logger-ი. * **გამოიყენეთ პარამეტრიზებული შეტყობინებები.** დაწერეთ `log.debug("User {} logged in", username)`, და არა `log.debug("User " + username + " logged in")`. პარამეტრიზებული შეტყობინებები თავიდან იცილებს სტრიქონების არასაჭირო კონკატენაციას, როდესაც დონე გამორთულია. * **აირჩიეთ სწორი დონე.** `DEBUG` დეველოპმენტის დროს გამოსადეგი ინფორმაციისთვის. `INFO` მნიშვნელოვანი მოვლენებისთვის (ჩართვა, ძირითადი ოპერაციები). `WARN` აღდგენადი პრობლემებისთვის, რომლებიც ყურადღებას იმსახურებს. `ERROR` მარცხებისთვის, რომლებიც მოითხოვს გამოძიებას. * **დაფიქრდით, რას ალოგირებთ.** ლოგის შეტყობინება უნდა იყოს გამოსადეგი ვინმესთვის, ვინც მას კვირების შემდეგ წაიკითხავს და შეეცდება გაიგოს, რა მოხდა. შეიტანეთ შესაბამისი კონტექსტი — იდენტიფიკატორები, რაოდენობები, ხანგრძლივობები — მაგრამ მოერიდეთ მგრძნობიარე მონაცემების ლოგირებას, როგორიცაა პაროლები, ტოკენები ან პერსონალური ინფორმაცია. * **დააკონფიგურირეთ ლოგების როტაცია.** ნებისმიერ გარემოში, რომელიც სცდება დეველოპერის მანქანას, დააკონფიგურირეთ გადახვევის (rolling) პოლიტიკა, რათა თავიდან აიცილოთ ლოგის ფაილების უსასრულოდ ზრდა. --- ## შეჯამება ამ თავში ჩვენ დავფარეთ ფუნდამენტური მექანიზმები, რომლებიც უზრუნველყოფენ Spring აპლიკაციების მუშაობას და გავეცანით logging-ის პრაქტიკებს, რომლებიც ამ აპლიკაციებს დაკვირვებადად (observable) ხდის. **ApplicationContext** არის Spring-ის IoC კონტეინერი — ის ქმნის bean-ებს, აინექცირებს დამოკიდებულებებს და მართავს მთელ სასიცოცხლო ციკლს ინსტანცირებიდან განადგურებამდე. **Inversion of Control** არის პრინციპი: ფრეიმვორქი მართავს ობიექტებს და არა თქვენი კოდი. **Dependency Injection** არის მექანიზმი: კლასს დამოკიდებულებები მიეწოდება გარედან, როგორც წესი, მისი კონსტრუქტორის მეშვეობით. როდესაც არსებობს ერთი და იმავე ტიპის მრავალი bean, **`@Primary`** ნიშნავს ნაგულისხმევს, **`@Qualifier`** ირჩევს კონკრეტულ bean-ს სახელის მიხედვით, ხოლო `List`-ის ინექცია აგროვებს ყველა იმპლემენტაციას. ყველა bean-ს აქვს **სასიცოცხლო ციკლი** — ინსტანცირება, ინექცია, ინიციალიზაცია (`@PostConstruct`), გამოყენება და განადგურება (`@PreDestroy`). bean-ებს აქვთ **scope-ები**, რომლებიც განსაზღვრავს რამდენი ინსტანსი არსებობს; ნაგულისხმევი singleton scope შესაფერისია კომპონენტების უმეტესობისთვის. **სტერეოტიპული ანოტაციები** (`@Component`, `@Service`, `@Repository`, `@Controller`) აღნიშნავენ კლასებს component scanning-ისთვის და გამოხატავენ არქიტექტურულ განზრახვას. მესამე მხარის კლასებისთვის ან მორგებული ინიციალიზაციისთვის, `@Bean` ანოტაცია `@Configuration` კლასების შიგნით უზრუნველყოფს ექსპლიციტურ bean განმარტებებს. **Auto-configuration** არის პირობითი `@Configuration` კლასების სისტემა, რომელსაც Spring Boot აფასებს გაშვებისას. ის აღმოაჩენს, რა არის classpath-ზე, ამოწმებს, რა გაქვთ უკვე განსაზღვრული და ავსებს დანარჩენს გონივრული ნაგულისხმევი პარამეტრებით. როდესაც გვერდს ესაჭიროება დინამიური მონაცემები, **`@Controller`** იღებს მას, ათავსებს `Model`-ში და აბრუნებს შაბლონის სახელს. FreeMarker იღებს model-ის ატრიბუტებს როგორც ცვლადებს და არენდერებს საბოლოო HTML-ს. **Logging** SLF4J-ით და Logback-ით ანაცვლებს `System.out.println()`-ს კონფიგურირებადი, სტრუქტურირებული, დონეებზე დაფუძნებული სისტემით. ლოგის დონეები (TRACE-დან ERROR-მდე) აკონტროლებს იმას, თუ რა იწერება. ლოგები შეიძლება ჩაიწეროს კონსოლში, ფაილებში ან ორივეში. პატერნები აკონტროლებს ფორმატს, ხოლო გადახვევის (rolling) პოლიტიკები ხელს უშლის შეუზღუდავ ზრდას. Lombok-ის `@Slf4j` ანოტაცია აღმოფხვრის logger-ის boilerplate კოდს. --- ## რესურსები * [Spring Framework Core — IoC Container](https://docs.spring.io/spring-framework/reference/core/beans.html) * [Spring Framework — Dependency Injection](https://docs.spring.io/spring-framework/reference/core/beans/dependencies.html) * [Spring Boot — Logging](https://docs.spring.io/spring-boot/reference/features/logging.html) * [SLF4J Manual](https://www.slf4j.org/manual.html) * [Logback Documentation](https://logback.qos.ch/manual/index.html) * [Apache FreeMarker Manual](https://freemarker.apache.org/docs/index.html) * [Spring MVC — FreeMarker Integration](https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-freemarker.html) --- ## პრაქტიკული დავალება: დაამატეთ Logging თქვენს Blog აპლიკაციას აიღეთ ბლოგის აპლიკაცია, რომელიც ააშენეთ 1-ლ თავში და გააფართოვეთ ის სათანადო ლოგირებით. **მოთხოვნები:** 1. **გააკეთეთ მინიმუმ ერთი გვერდის რეფაქტორინგი, რათა გამოიყენოთ `@Controller**` view controller-ის ნაცვლად. controller-მა უნდა ამოიღოს მონაცემები (ჯერჯერობით შეიძლება იყოს ხელით გაწერილი (hardcoded) — მაგალითად, ბლოგის პოსტების სია), მოათავსოს ის `Model`-ში და დააბრუნოს შაბლონის სახელი. შაბლონმა უნდა აჩვენოს მონაცემები `${...}` და `<#list>`-ის გამოყენებით. 2. **დაამატეთ logging თქვენს controller-სა და ნებისმიერ service კლასში.** გამოიყენეთ Lombok-ის `@Slf4j` ანოტაცია. აწარმოეთ ლოგირება მრავალ დონეზე: `INFO` ძირითადი ოპერაციებისთვის (მაგ., "Retrieving all posts"), `DEBUG` დეტალებისთვის (მაგ., "Found 5 posts") და `WARN` ან `ERROR` გამონაკლისი სიტუაციებისთვის. 3. **დააკონფიგურირეთ log-ის დონეები** `application.properties`-ში: დააყენეთ root დონე `INFO`-ზე, ხოლო თქვენი აპლიკაციის პაკეტისთვის დააყენეთ `DEBUG`. 4. **დააკონფიგურირეთ ფაილში ლოგირება.** დააყენეთ `logging.file.name` ისე, რომ ლოგები ჩაიწეროს ფაილში კონსოლთან ერთად. 5. **მოარგეთ (Customize) log-ის პატერნი** კონსოლისთვის ან ფაილისთვის (ან ორივესთვის). 6. **არასავალდებულო:** შექმენით `logback-spring.xml` კონფიგურაცია rolling file appender-ით, რომელიც ატრიალებს ლოგებს ყოველდღიურად და ინახავს არაუმეტეს 7 დღის ისტორიას.