ویرگول
ورودثبت نام
نیما
نیما
نیما
نیما
خواندن ۱۴ دقیقه·۲ ماه پیش

مقدمه‌ای بر Spring Core و کانتینر IoC

اسپرینگ چیست و چرا به آن نیاز داریم؟

تصور کنید در حال ساختن یک ماشین هستید. در حالت عادی، شما باید خودتان بروید کارخانه لاستیک‌سازی، لاستیک بخرید. بروید کارخانه موتور سازی، موتور بخرید و… . سپس خودتان این قطعات را به هم وصل کنید. این کار بسیار زمان‌بر و پیچیده است.

اسپرینگ (Spring) مانند یک کارخانه ماشین‌سازی فوق پیشرفته است.

شما به این کارخانه نمی‌گویید “چگونه” موتور بسازد. شما فقط یک “نقشه” (پیکربندی) به آن می‌دهید و می‌گویید: “من یک ماشین می‌خواهم که یک موتور مدل X و چهار لاستیک مدل Y داشته باشد.”

کارخانه (که در اسپرینگ به آن کانتینر IoC یا ApplicationContext می‌گوییم) خودش می‌رود و تمام این قطعات (که در اسپرینگ به آن‌ها Bean می‌گوییم) را می‌سازد و به درستی به هم وصل می‌کند (این فرآیند اتصال را تزریق وابستگی یا Dependency Injection می‌نامیم) و در نهایت یک ماشین آماده و کامل به شما تحویل می‌دهد.

1 . وارونگی کنترل (Inversion of Control - IoC): کنترل ساخت و اتصال قطعات از شما گرفته شده و به کارخانه (اسپرینگ) واگذار شده است.

  • در برنامه‌نویسی سنتی، شما مسئول ساختن و مدیریت اشیاء (objects) هستید. مثلاً new MyService() را خودتان می‌نویسید. در Spring، این کنترل از شما گرفته شده و به یک “کانتینر” (Container) سپرده می‌شود. شما فقط به کانتینر می‌گویید چه اشیائی نیاز دارید و چگونه باید به هم متصل شوند؛ خود کانتینر آن‌ها را می‌سازد و مدیریت می‌کند.

2 . تزریق وابستگی ( Dependency Injection - DI): کارخانه، موتور را داخل شاسی “تزریق” می‌کند. شما خودتان این کار را انجام نمی‌دهید.

  • این یک پیاده‌سازی مشخص از اصل IoC است. وقتی یک شیء (مثلاً OrderService) به شیء دیگری (مثلاً PaymentGateway) نیاز دارد، به جای اینکه OrderService خودش PaymentGateway را بسازد، کانتینר Spring این وابستگی را از بیرون به آن “تزریق” می‌کند.

این کانتینر در Spring با نام ApplicationContext شناخته می‌شود و مسئولیت مدیریت کل چرخه حیات اشیاء شما را بر عهde دارد. به اشیائی که توسط کانتینר Spring مدیریت می‌شوند، Bean گفته می‌شود.

حالا با این دید، 12 سرفصل‌ها را یکی یکی بررسی می‌کنیم.


۱. نمونه‌سازی و پیکربندی (Instantiation and Configuration)

این بخش یعنی: “چگونه به کارخانه اسپرینگ (کانتینر) نقشه ساخت Beanها را بدهیم؟”

در روش مدرن، دو راه اصلی برای این کار وجود دارد:

روش اول: اسکن خودکار (Component Scanning)

این روش، ساده‌ترین و رایج‌ترین راه است. شما بالای کلاس‌های جاوا خود یک علامت خاص (Annotation) می‌گذارید و اسپرینگ خودش آن‌ها را پیدا کرده و به عنوان Bean ثبت می‌کند.

مثل این است که روی قطعاتی که می‌خواهید در ماشین استفاده شوند، یک برچسب “استفاده شود” بچسبانید.

اصلی‌ترین این علامت‌ها عبارتند از:

  • @Component: یک علامت عمومی برای هر نوع کلاسی.

  • @Service: برای کلاس‌هایی که منطق کسب‌وکار (Business Logic) را انجام می‌دهند (مثلاً UserService).

  • @Repository: برای کلاس‌هایی که با پایگاه داده کار می‌کنند (مثلاً UserRepository).

  • @Controller: برای کلاس‌هایی که درخواست‌های وب را مدیریت می‌کنند (در بخش Spring MVC کاربرد دارد).

این چهار Annotation از نظر فنی کار یکسانی انجام می‌دهند، اما استفاده از آن‌ها به تفکیک، خوانایی کد را بسیار بالا می‌برد.

مثال:

javapackage com.example.services; import org.springframework.stereotype.Service; @Service // به اسپرینگ می‌گوییم این یک Bean از نوع Service است public class NotificationService { public void sendNotification(String message) { System.out.println("Sending: " + message); } }

حالا فقط کافیست به اسپرینگ بگوییم که بسته (package) com.example.services را برای پیدا کردن این علامت‌ها اسکن کند.

روش دوم: پیکربندی صریح با جاوا (Java-based Configuration)

گاهی اوقات شما نمی‌توانید کد یک کلاس را تغییر دهید (مثلاً یک کتابخانه خارجی است) یا ساخت یک شیء منطق پیچیده‌ای دارد. در این حالت، شما خودتان به صورت صریح در یک کلاس مخصوص، به اسپرینگ می‌گویید که چگونه آن Bean را بسازد.

این کلاس مخصوص با @Configuration علامت‌گذاری می‌شود و متدهایی که Bean تولید می‌کنند با @Bean علامت‌گذاری می‌شوند.

مثال:

javapackage com.example.config; import com.example.services.NotificationService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration // این یک کلاس پیکربندی است public class AppConfig { @Bean // این متد یک Bean به نام notificationService تولید می‌کند public NotificationService notificationService() { // شما کنترل کامل روی ساخت شیء دارید return new NotificationService(); } }

انواع روش‌های تزریق وابستگی (DI)

حالا که اسپرینگ می‌داند چه Beanهایی را بسازد، سوال بعدی این است که چگونه آن‌ها را به هم وصل کند؟ مثلاً اگر OrderService به NotificationService نیاز دارد، اسپرینگ چگونه این دو را به هم متصل می‌کند؟ اینجاست که تزریق وابستگی وارد می‌شود.

علامت اصلی برای درخواست تزریق، @Autowired است.

سه روش اصلی برای تزریق وجود دارد که شما تفاوت‌هایشان را پرسیده بودید:

۱. تزریق از طریق سازنده (Constructor Injection) - روش پیشنهادی

در این روش، وابستگی‌ها به عنوان پارامتر به سازنده (Constructor) کلاس پاس داده می‌شوند.

public class OrderService { private final NotificationService notificationService; // وابستگی // اسپرینگ به صورت خودکار دنبال یک Bean از نوع NotificationService می‌گردد // و آن را به این سازنده پاس می‌دهد. @Autowired // در نسخه‌های جدید اسپرینگ، اگر کلاس فقط یک سازنده داشته باشد، این Annotation اختیاری است public OrderService(NotificationService notificationService) { this.notificationService = notificationService; } public void placeOrder() { System.out.println("Order placed."); // استفاده از وابستگی تزریق شده notificationService.sendNotification("Your order was successful!"); } }
  • کجا استفاده کنیم؟ این روش بهترین و پیشنهادی‌ترین روش است. به خصوص برای وابستگی‌های ضروری (Mandatory).

  • چرا بهتر است؟

  • تضمین سلامت شیء: شیء OrderService هرگز بدون NotificationService ساخته نمی‌شود. همیشه در یک وضعیت کامل و معتبر قرار دارد.

  • تغییرناپذیری (Immutability): می‌توانید فیلد وابستگی را final تعریف کنید که امنیت و پایداری کد را بالا می‌برد.

  • خوانایی و سادگی تست: با یک نگاه به سازنده، می‌فهمید کلاس به چه چیزهایی نیاز دارد. همچنین در تست‌ها، به راحتی می‌توانید یک شیء Mock (تقلبی) از وابستگی را به سازنده پاس دهید بدون نیاز به اسپرینگ.

۲. تزریق از طریق Setter (Setter Injection)

در این روش، اسپرینگ ابتدا شیء را با سازنده پیش‌فرض می‌سازد و سپس متد setter را برای تزریق وابستگی فراخوانی می‌کند.

@Service public class OrderService { private NotificationService notificationService; @Autowired // اسپرینگ این متد را با یک Bean از نوع NotificationService فراخوانی می‌کند public void setNotificationService(NotificationService notificationService) { this.notificationService = notificationService; } // ... متدهای دیگر }
  • کجا استفاده کنیم؟ برای وابستگی‌های اختیاری (Optional) که ممکن است در زمان اجرا تغییر کنند یا وجود نداشته باشند.

  • تفاوت اصلی: این روش اجازه می‌دهد شیء بدون وابستگی‌اش ساخته شود و وابستگی بعداً تزریق گردد. این انعطاف‌پذیری گاهی مفید است اما اصل “تضمین سلامت شیء” را نقض می‌کند.

۳. تزریق مستقیم در فیلد (Field Injection) - روشی که باید از آن اجتناب کرد

در این روش، @Autowired مستقیماً بالای تعریف فیلد قرار می‌گیرد.

@Service public class OrderService { @Autowired private NotificationService notificationService; // تزریق مستقیم // ... }
  • کجا استفاده کنیم؟ سعی کنید هرگز از این روش استفاده نکنید! گرچه کد را کوتاه‌تر می‌کند، اما مشکلات جدی ایجاد می‌کند.

  • چرا بد است؟

  • نقض اصول طراحی: وابستگی‌های کلاس را پنهان می‌کند.

  • دشواری در تست: برای تست این کلاس، شما نمی‌توانید به سادگی یک Mock به فیلد تزریق کنید و مجبور به استفاده از reflection یا اجرای کامل کانتینر اسپرینگ می‌شوید که تست واحد (Unit Test) را بسیار سخت می‌کند.

  • عدم امکان final: نمی‌توانید فیلد را final تعریف کنید.

  • روش پیشنهادی : تزریق از طریق سازنده (Constructor Injection).

  • رفتار فنی @Autowired: ابتدا بر اساس نوع (byType) و در صورت ابهام، بر اساس نام (byName).


۳. ApplicationContext چیست؟

اگر اسپرینگ را یک کارخانه ماشین‌سازی در نظر بگیریم، ApplicationContext مغز متفکر و مدیر کل کارخانه است.

این یک اینترفیس (Interface) در اسپرینگ است که تمام کارهای اصلی را مدیریت می‌کند. ApplicationContext قلب تپنده برنامه اسپرینگ شماست.

وظایف اصلی ApplicationContext:

  1. خواندن پیکربندی (Reading Configuration): اولین کارش این است که “نقشه ساخت” را بخواند. این نقشه می‌تواند کلاس‌های دارای @Configuration یا کلاس‌های علامت‌گذاری شده با @Component و امثال آن باشد.

  2. ساخت و مدیریت Beanها (Bean Creation & Management): بر اساس نقشه، تمام اشیاء (Beanها) مورد نیاز شما را می‌سازد.

  3. تزریق وابستگی‌ها (Dependency Injection): قطعات مختلف را طبق نقشه به هم وصل می‌کند. مثلاً NotificationService را به OrderService تزریق می‌کند.

  4. مدیریت چرخه حیات (Lifecycle Management): مسئول اجرای متدهای @PostConstruct (بعد از ساخت) و @PreDestroy (قبل از نابودی) برای هر Bean است.

  5. ارائه خدمات پیشرفته: علاوه بر این‌ها، خدمات دیگری مانند مدیریت رویدادها (Event Publishing)، پشتیبانی از چندزبانه بودن (Internationalization) و بارگذاری منابع (Resource Loading) را نیز فراهم می‌کند.

به طور خلاصه، ApplicationContext همان چیزی است که ما به آن کانتینر IoC اسپرینگ می‌گوییم. بدون آن، هیچ‌کدام از جادوهای اسپرینگ اتفاق نمی‌افتد.

در یک برنامه ساده، شما ممکن است اینطور آن را راه‌اندازی کنید:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); // حالا می‌توانید یک Bean را از کانتینر درخواست کنید OrderService myOrderService = context.getBean(OrderService.class); myOrderService.placeOrder();

(البته در برنامه‌های بزرگتر مثل Spring Boot، این راه‌اندازی به صورت خودکار برای شما انجام می‌شود.)


۴. ارث‌بری در پیکربندی Bean

این هم مفهومی است که در XML بسیار رایج بود (parent attribute) تا از تکرار پیکربندی‌های مشترک جلوگیری شود. هدف، اصل DRY (Don’t Repeat Yourself) است.

در روش مدرن با Java Config، ما به شکل بسیار طبیعی‌تر و خواناتری به همین هدف می‌رسیم: با استفاده از متدهای کمکی.

فرض کنید دو Bean دارید که تنظیمات مشترکی دارند.

@Configuration public class AppConfig { // متد کمکی برای تنظیمات مشترک private void configureCommonProperties(BaseUser user) { user.setCountry("Iran"); user.setRegistrationDate(LocalDate.now()); } @Bean public User adminUser() { AdminUser admin = new AdminUser(); // فراخوانی متد مشترک configureCommonProperties(admin); // تنظیمات خاص این Bean admin.setPermissions("ALL"); return admin; } @Bean public User normalUser() { NormalUser normal = new NormalUser(); // فراخوانی متد مشترک configureCommonProperties(normal); // تنظیمات خاص این Bean normal.setPermissions("READ_ONLY"); return normal; } }

همانطور که می‌بینید، با یک متد private ساده، به همان نتیجه ارث‌بری در XML رسیدیم، اما با کدی خواناتر و قابل نگهداری‌تر.


۶. پردازشگرهای پس از Bean (Bean Post-Processors)

این یکی از جادویی‌ترین بخش‌های اسپرینگ است!

تصور کنید در کارخانه ماشین‌سازی شما، یک “بازرس کنترل کیفیت” وجود دارد که هر ماشینی که از خط تولید خارج می‌شود را بررسی می‌کند. این بازرس می‌تواند ماشین را همانطور که هست تایید کند، یا یک تغییر کوچک روی آن اعمال کند (مثلاً یک برچسب اضافه کند)، یا حتی آن را در یک پوشش ضد آب قرار دهد!

BeanPostProcessor دقیقاً همین بازرس است. این یک اینترفیس است که به شما اجازه می‌دهد قبل و بعد از مقداردهی اولیه هر Bean، یک کد دلخواه را اجرا کنید.

چرا اینقدر مهم است؟

بسیاری از قابلیت‌های شگفت‌انگیز اسپرینگ مانند مدیریت تراکنش (@Transactional)، اجرای ناهمزمان متد (@Async) و امنیت، با استفاده از همین مکانیزم پیاده‌سازی شده‌اند. اسپرینگ با استفاده از یک BeanPostProcessor، Bean اصلی شما را در یک شیء دیگر به نام Proxy می‌پیچد و قابلیت‌های اضافی را در آن پراکسی پیاده می‌کند.

مثال ساده: بیایید یک بازرس بنویسیم که نام هر Bean که ساخته می‌شود را چاپ کند.

import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; @Component // مهم: خود بازرس هم باید یک Bean باشد تا اسپرینگ آن را بشناسد! public class CustomBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("-> بازرس قبل از مقداردهی اولیه Bean '" + beanName + "' وارد عمل شد."); return bean; // حتما باید خود Bean را برگردانید } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("-> بازرس بعد از مقداردهی اولیه Bean '" + beanName + "' وارد عمل شد."); return bean; // حتما باید خود Bean را برگردانید } }

فقط کافیست این کلاس در پروژه شما وجود داشته باشد و اسپرینگ آن را به عنوان یک Bean بشناسد (با @Component). اسپرینگ به طور خودکار آن را روی تمام Beanهای دیگر اعمال خواهد کرد.


۷. قلاب‌های چرخه حیات (Lifecycle Hooks)

هر Bean در اسپرینگ یک چرخه حیات دارد: ساخته می‌شود، مقداردهی اولیه می‌شود، و در نهایت از بین می‌رود. اسپرینگ به شما “قلاب‌هایی” می‌دهد تا در لحظات کلیدی این چرخه، کدهای خودتان را اجرا کنید.

این قابلیت برای کارهایی مثل باز کردن یک اتصال به دیتابیس (در زمان شروع) و بستن آن (در زمان پایان) حیاتی است.

روش مدرن و پیشنهادی: استفاده از Annotationهای @PostConstruct و @PreDestroy.

  • @PostConstruct: متدی که با این علامت مشخص شود، بلافاصله بعد از ساخته شدن Bean و تزریق تمام وابستگی‌هایش، فراخوانی می‌شود. این بهترین مکان برای کارهای مقداردهی اولیه است.

  • @PreDestroy: متدی که با این علامت مشخص شود، درست قبل از اینکه کانتینر اسپرینگ خاموش شود و Bean را از بین ببرد، فراخوانی می‌شود. این بهترین مکان برای آزادسازی منابع است.

مثال:

import javax.annotation.PreDestroy; @Component public class ConnectionManager { @PostConstruct public void init() { System.out.println("Bean ساخته شد. در حال اتصال به منابع..."); // مثلا: باز کردن کانکشن دیتابیس یا فایل } @PreDestroy public void cleanup() { System.out.println("Bean در حال نابودی است. در حال آزادسازی منابع..."); // مثلا: بستن کانکشن دیتابیس یا فایل } }

این روش بسیار تمیز است چون کد شما به هیچ اینترفیس خاصی از اسپرینگ وابسته نمی‌شود.


۸. یکپارچه‌سازی با کدهای Factory موجود

گاهی شما با کدهای قدیمی کار می‌کنید که از الگوی طراحی Factory برای ساختن اشیاء استفاده می‌کنند و شما نمی‌توانید کد آن‌ها را تغییر دهید. اسپرینگ به راحتی می‌تواند با آن‌ها کار کند.

در روش Java Config این کار فوق‌العاده ساده است:

حالت ۱: متد Factory استاتیک

کلاسی که یک متد static برای برگرداندن نمونه دارد.

// کد قدیمی که قابل تغییر نیست public class LegacyServiceFactory { public static LegacyService createInstance() { return new LegacyService(); } }

پیکربندی در اسپرینگ:

@Configuration public class AppConfig { @Bean public LegacyService legacyService() { // به سادگی متد استاتیک را فراخوانی کنید! return LegacyServiceFactory.createInstance(); } }

حالت ۲: متد Factory غیر استاتیک (Instance)

ابتدا باید یک نمونه از خود Factory بسازید.

// کد قدیمی که قابل تغییر نیست public class PaymentGatewayFactory { public IPaymentGateway createGateway(String type) { // منطق ساخت... } }

پیکربندی در اسپرینگ:

public class AppConfig { @Bean public PaymentGatewayFactory paymentGatewayFactory() { return new PaymentGatewayFactory(); } @Bean public IPaymentGateway paypalGateway(PaymentGatewayFactory factory) { // خود فکتوری را تزریق می‌کنیم // و متد آن را برای ساخت Bean نهایی فراخوانی می‌کنیم return factory.createGateway("paypal"); } }

۹. اینترفیس‌های آگاهی (Awareness Interfaces)

این اینترفیس‌ها به Bean شما اجازه می‌دهند از محیط اجرایی خود “آگاه” شود. یعنی اسپرینگ اطلاعاتی درباره کانتینر را به خود Bean تزریق می‌کند.

معروف‌ترین آن‌ها ApplicationContextAware است که به Bean شما اجازه می‌دهد به خود کانتینر اسپرینگ (ApplicationContext) دسترسی داشته باشد.

import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class MyAwareBean implements ApplicationContextAware { private ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("اسپرینگ، کانتینر را به من داد!"); this.context = applicationContext; } public void doSomethingAdvanced() { // حالا می‌توانم به صورت دستی یک Bean دیگر را از کانتینر درخواست کنم OrderService orderService = context.getBean(OrderService.class); orderService.placeOrder(); } }

یک هشدار مهم: استفاده از این اینترفیس‌ها، به خصوص ApplicationContextAware، معمولاً یک ضد الگو (Anti-Pattern) محسوب می‌شود. چرا؟ چون اصل وارونگی کنترل را نقض می‌کند. به جای اینکه وابستگی‌ها به Bean تزریق شوند، خود Bean به دنبال وابستگی‌هایش در کانتینر می‌گردد. این کار کد شما را به شدت به اسپرینگ وابسته کرده و تست آن را سخت‌تر می‌کند.

پس کی استفاده کنیم؟ فقط در موارد بسیار نادر و خاص، مثلاً زمانی که نیاز دارید به صورت داینامیک و برنامه‌نویسی شده به Beanها دسترسی پیدا کنید. در ۹۹٪ مواقع، شما به این قابلیت نیازی ندارید و باید از تزریق وابستگی معمولی استفاده کنید.

امیدوارم این توضیحات جامع و متناسب با درخواست شما بوده باشد. بهترین راه برای یادگیری، ساختن یک پروژه کوچک و تست کردن تک‌تک این مفاهیم است. موفق باشید

10. محدوده حیات Bean (Bean Scopes)

به صورت پیش‌فرض، هر Bean که در اسپرینگ تعریف می‌کنید Singleton است. یعنی اسپرینگ فقط یک نمونه از آن Bean را در کل کانتینر می‌سازد و هر بار که کسی آن را درخواست کند، همان یک نمونه را تحویل می‌دهد.

اما اسکوپ‌های دیگری هم وجود دارند:

  • prototype: هر بار که این Bean درخواست شود، اسپرینگ یک نمونه کاملاً جدید از آن می‌سازد. مثل اینکه هر بار یک ماشین جدید از خط تولید خارج شود.

  • request: (در برنامه‌های وب) یک نمونه از Bean به ازای هر درخواست HTTP ساخته می‌شود.

  • session: (در برنامه‌های وب) یک نمونه از Bean به ازای هر سشن کاربر ساخته می‌شود.

دانستن تفاوت singleton و prototype در همین سطح بسیار مهم است.

content_copy javaimport org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Component @Scope("prototype") // هر بار یک نمونه جدید از این کلاس ساخته می‌شود public class ShoppingCart { // ... }

11. پروفایل‌ها (Profiles)

شما تقریباً هرگز یک برنامه را با یک پیکربندی یکسان برای محیط توسعه (Development)، تست (Test) و محصول نهایی (Production) اجرا نمی‌کنید. مثلاً در محیط توسعه از یک دیتابیس درون‌حافظه‌ای (In-memory) استفاده می‌کنید، اما در محیط محصول نهایی به یک دیتابیس واقعی متصل می‌شوید.

پروفایل‌ها به شما اجازه می‌دهند Beanها یا کلاس‌های پیکربندی را برای محیط‌های خاص فعال یا غیرفعال کنید.

content_copy java@Configuration public class DataSourceConfig { @Bean @Profile("development") // این Bean فقط زمانی فعال است که پروفایل 'development' فعال باشد public DataSource inMemoryDataSource() { // ... ساخت دیتابیس درون حافظه‌ای } @Bean @Profile("production") // این Bean فقط زمانی فعال است که پروفایل 'production' فعال باشد public DataSource productionDataSource() { // ... ساخت دیتابیس واقعی } }

12. پیکربندی خارجی (Externalized Configuration)

هرگز مقادیر حساس یا مقادیری که ممکن است تغییر کنند (مانند آدرس دیتابیس، پسوردها، کلیدهای API) را مستقیماً در کد جاوا خود ننویسید (Hardcode نکنید).

اسپرینگ به شما اجازه می‌دهد این مقادیر را در یک فایل خارجی (مثلاً application.properties) قرار دهید و سپس آن‌ها را در Beanهای خود تزریق کنید.

فایل application.properties:

content_copy propertiesapp.name=My Awesome App notification.service.url=https://api.example.com/notify

تزریق مقادیر در یک Bean:

content_copy javaimport org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class AppInfo { @Value("${app.name}") // مقدار را از فایل properties بخوان private String applicationName; @Value("${notification.service.url}") private String notificationUrl; public void printInfo() { System.out.println("App Name: " + applicationName); System.out.println("Notification URL: " + notificationUrl); } }

این قابلیت برای ساخت برنامه‌های واقعی و قابل نگهداری، حیاتی است.

اسپرینگdi
۱
۰
نیما
نیما
شاید از این پست‌ها خوشتان بیاید