MVC SPRING واقعا چگونه کار می کند؟ (قسمت ۲)
در ادامه مطلب قبلی، در اینجا پردازش یک درخواست HTTP را بررسی میکنیم.
پردازش یک درخواست HTTP
بیایید در ابتدا روال پردازش یک درخواست ساده HTTP به یک متد در لایه کنترلر و برگشت آن به مرورگر / کلاینت را دنبال کنیم.
DispatcherServlet سلسله مراتبی طولانی دارد؛ یک به یک این منظرها ارزش فهمیدن دارند اما متدهای پردازش درخواست بیشتر مورد توجه ما هستند.
شناخت درخواستهای HTTP، هم به شکل لوکال حین توسعه استاندارد، و همچنین از راه دور، بخش مهمی از درک معماری MVC است.
GenericServlet
GenericServlet بخشی از مشخصات Servlet است که به طور مستقیم بر روی HTTP متمرکز نیست. این بخش متد () service را تعریف میکند که درخواستهای وارد شده را گرفته و پاسخها را تولید میکند.
توجه داشته باشید که چگونه آرگومانهای ServletRequest و ServletResponse به پروتکل HTTP متصل نشدند:
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
این متدی است که در نهایت بر روی هر درخواست سرور، از جمله درخواست ساده GET فراخوانی میشود.
HttpServlet
کلاس HttpServlet، همانطور که از نامش بر میآید، پیادهسازی Servlet متمرکز بر HTTP است که با مشخصات (specification) هم تعریف شده است.
در شرایط عملیتر، HttpServlet یک کلاس انتزاعی با متد service است که درخواستها را بر اساس نوع متد HTTP تقسیم میکند و تقریبا شبیه این است:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { // ... doGet(req, resp); } else if (method.equals(METHOD_HEAD)) { // ... doHead(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); // ... }
HttpServletBean
HttpServletBean اولین کلاس آگاه از Spring در این سلسله مراتب است. این کلاس مشخصات Bean را با استفاده از مقادیر init-param servlet که از web.xml یا از WebApplicationInitializer دریافت میکند. تزریق میکند.
در صورت درخواست به برنامه شما، متدهای doGet، doPost برای این درخواستهای خاص HTTP فراخوانی میشوند.
FrameworkServlet
Framework Servlet با پیادهسازی واسط ApplicationContextAware، قابلیت Servlet را با context یک برنامه وب یکپارچه میکند. علاوهبر این قادر است یک context برنامه وب به تنهایی ایجاد کند. همانطور که قبلا مشاهده کردید، سوپر کلاس HttpServletBean پارامترهای init-param سرولت را به عنوان خصوصیات Bean تزریق میکند. بنابراین، اگر نام کلاس context در init-param در contextClass سرولت ارائه شده باشد، یک نمونه از این کلاس به عنوان یک context برنامه ساخته میشود. در غیر این صورت، یک کلاس پیش فرض XmlWebApplicationContext استفاده میشود.
همانطور که پیکربندی XML در حال حاضر قدیمی شده است ، Spring Boot به طور پیش فرض DispatcherServlet را با AnnotationConfigWebApplicationContext پیکربندی میکند. اما شما میتوانید آن را به آسانی تغییر دهید.
برای مثال، اگر لازم دارید که برنامه Spring Web MVC خود را با یک context برنامه مبتنی بر Groovy پیکربندی کنید، میتوانید از تنظیمات ServletDispatcher زیر در فایل web.xml استفاده کنید:
dispatcherServlet org.springframework.web.servlet.DispatcherServlet contextClass org.springframework.web.context.support.GroovyWebApplicationContext
همان پیکربندی را میتوان با استفاده از کلاس WebApplicationInitializer با یک روش جدیدتر مبتنی بر جاوا انجام داد.
DispatcherServlet: ادغام پردازش درخواست
پیادهسازی HttpServlet.service که بر اساس نوع عملکرد HTTP، درخواستها را مسیردهی میکند، در زمینه Servletهای سطح پایین ملموستر است. با این حال، در سطح انتزاعی Spring MVC ، نوع متد فقط یکی از پارامترهایی است که می تواند برای نگاشت درخواست به handler آن استفاده شود.
و همینطور، کارایی اصلی دیگر کلاس FrameworkServlet، اتصال منطق handling به یک متد تنهای processRequest است که خود تابع doService را صدا میزند:
@Override protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } @Override protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { processRequest(request, response); } // …
DispatcherServlet: غنیسازی درخواست
در نهایت DispatcherServlet تابع doService را پیادهسازی میکند. در اینجا، به درخواست، برخی از اشیایی که ممکن است در جریان پردازش مفید باشد، اضافه می شود: context برنامه وب، locale resolver، theme resolver، منبع theme و غیره:
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
همچنین، این تابع Flash Mapهای ورودی و خروجی را آماده می کند. اساسا flash map یک الگو برای انتقال پارامترها از یک درخواست به درخواست دیگری است که بلافاصله دنبال میشود. این ممکن است در هنگام تغییر مسیر بسیار مفید باشد (مانند نشان دادن یک پیام اطلاعات به کاربر پس از تغییر مسیر) :
FlashMap inputFlashMap = this.flashMapManager .retrieveAndUpdate(request, response); if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); } request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
سپس، متد doService تابع doDispatch که مسئول انتقال درخواست است را فراخوانی میکند.
DispatcherServlet: ارسال درخواست
هدف اصلی تابع dispatch یافتن یک handler مناسب برای درخواست و ارسال پارامترهای (درخواست / پاسخ) به آن است. Handler اساسا هر نوع Object است و به یک واسط خاصی محدود نمیشود. همچنین به این معنی است که Spring لازم است یک آداپتور برای این handler پیدا کند که بداند چطور با آن صحبت کند.
به منظور پیدا کردن handler مطابق با درخواست، Spring از طریق پیادهسازی های ثبت شده واسط HandlerMapping اقدام میکند. پیادهسازیهای مختلفی وجود دارد که میتوانتد با نیازهای شما منطبق باشد.
SimpleUrlHandlerMapping اجازه میدهد نگاشت یک درخواست توسط URL آن به یک processing bean خاص انجام گیرد. به عنوان مثال، می توان آن را با تزریق مشخصه mappings با یک نمونه از java.util.Properties مشابه زیر انجام داد.
/welcome.html=ticketController /show.html=ticketController
شاید پراستفادهترین کلاسی که برای نگاشت handler استفاده میشود RequestMappingHandlerMapping است، که درخواست را به یک متد annotate شده با RequestMapping از کلاس Controller@ نگاشت میکند. این دقیقا همان نگاشتی است که با متدهای hello و login،ی dispatcher را به کنترلر متصل میکند.
توجه داشته باشید که متدهای Spring-aware شما با GetMapping@ و @PostMappingcorrespondingly حاشیهنویسی (annotate) شدهاند. اینها خود با متا-انوتیشن @RequestMapping حاشیهنویسی شدهاند.
تابع dispatch نیز به برخی از دیگر وظایف خاص HTTP توجه دارد:
- اتصال کوتاه کردن پردازش درخواست GET در صورتی که منبع تعیین نشده باشد
- اعمال resolver چندبخشی برای درخواستهای مربوطه
- اتصال کوتاهکردن پردازش درخواستها در صورتی که handler تصمیم اجرائ آن را به صورت asynchron گرفته باشد
رسیدگی به درخواست
حالا که Spring برای درخواست handler و برای handler آداپتور را تعیین کرد، وقت آن است که در نهایت درخواست را handle کنیم. در اینجا امضای تایع HandlerAdapter.handle را میبینید. مهم است که دقت داشته باشید که handler در چگونگی رسیدگی به درخواست، حق انتخاب دارد:
- خود داده ها را به شی پاسخ بسپرد و null بازگرداند
- شی ModelAndView را برگرداند که توسط DispatcherServlet رندر شده است.
@Nullable ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
چند نوع handler وجود دارد .در اینجا نحوه پردازش یک کنترلر Spring MVC را توسط یک نمونه از SimpleControllerHandlerAdapter میبینید (با @Controller-annotated POJO اشتباه نگیرید).
توجه کنید که چگونه کنترل کننده handler شیء ModelAndView را برمیگرداند و خودش نمایش نمیدهد:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return ((Controller) handler).handleRequest(request, response); }
دومی SimpleServletHandlerAdapter است که یک سرولت معمولی را به عنوان هندلر درخواست استفاده میکند.
سرولت هیچ چیز در مورد ModelAndView نمیداند و به سادگی درخواست را خود هندل میکند و نتیجه را به شی پاسخ میدهد. بنابراین این آداپتور به جای ModelAndView به null ختم میشود:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ((Servlet) handler).service(request, response); return null; }
در مورد شما، کنترل کننده یک POJO با چندین حاشیهنویسی @RequestMapping است،بنابراین هر handler اساسا یک تابع از این کلاس است که در نمونه HandlerMethod پیچیده شده است. برای سازگاری با این نوع هندلر Spring از کلاس RequestMappingHandlerAdapter استفاده میکند.
Arguments پردازش و ارزشهای بازگشت از روش Handler
توجه داشته باشید که توابع کنترلر معمولا آرگومانهای HttpServletRequest و HttpServletResponse را نمیگیرند، بلکه به جای آن به دریافت و بازگرداندن انواع مختلفی از دادهها، مانند اشیاء دامنه، پارامترهای مسیر و غیره میپردازند.
همچنین توجه داشته باشید که شما نیازی به بازگشت نمونه ModelAndView از یک تابع کنترلر ندارید. شما می توانید یک نام view، یا یک ResponseEntity یا یک POJO که به پاسخ JSON تبدیل می شود، برگردانید.
RequestMappingHandlerAdapter ، اطمینان میدهد که آرگومانهای تابع از HttpServletRequest به دست میآید. همچنین، شیء ModelAndView را از مقدار برگشتی متد میسازد.
یک تکه کد مهم در RequestMappingHandlerAdapter وجود دارد که اطمینان حاصل می کند که تمام این تبدیلات صورت میگیرد:
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers( this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers( this.returnValueHandlers); }
شي argumentResolvers ترکیبی از نمونههای مختلف HandlerMethodArgumentResolver است.
بیش از 30 پیادهسازی مختلف argument resolver وجود دارد. آنها اجازه استخراج هرگونه اطلاعات از درخواست و ارائه آن به عنوان آرگومان تابع را میدهند . این شامل متغیرهای مسیر URL، پارامترهای بدنه درخواست، هدر درخواست، کوکیها، دادههای جلسه و غیره میباشد .
شئ returnValueHandlers ترکیبی از اشیاء HandlerMethodReturnValueHandler است. همچنین تعداد زیادی value handler وجود دارد که میتواند نتیجه تابع شما را پردازش کند تا شئ ModelAndView مورد انتظار آداپتور ایجاد شود.
به عنوان مثال، هنگامی که یک رشته را از تابع hello باز میگردانید، ViewNameMethodReturnValueHandler مقدار را پردازش میکند. اما وقتی ModelAndView را از تابع login باز میگردانید، Spring از ModelAndViewMethodReturnValueHandler استفاده میکند.
رندر کردن View
در حال حاضر، Spring درخواست HTTP را پردازش کرده و یک شئ ModelAndView را دریافت کرده است، بنابراین باید صفحه HTML را که کاربر در مرورگر مشاهده میکند، رندر کند. این کار بر اساس مدل و view انتخاب شدهای که در شی ModelAndView انکپسولیت شده انجام میگیرد.
همچنین توجه داشته باشید که میتوانید شئ JSON، یا XML یا هر فرمت داده دیگر را که میتوانید از طریق پروتکل HTTP منتقل کنید، رندر کنید. ما در مورد این در آینده در بخش REST تمرکز خواهیم کرد.
بیایید به DispatcherServlet بازگردیم. تابع render برای اولین بار محل یابی پاسخ را با استفاده از مثال ارائه شده LocaleResolver تنظیم میکند. فرض کنیم مرورگر مدرن شما هدر Accept صحیح را تعیین میکند و AcceptHeaderLocaleResolver به طور پیش فرض استفاده میشود.
در حین رندر، شئ ModelAndView میتواند از قبل رفرنسی داشته باشد به یک view انتخاب شده یا فقط نام view یا هیچکدام در صورت استفاده از view پیش فرض.
از آنجا که هر دو تابع hello و login، دید یا view دلخواه خود را به عنوان نام String مشخص میکنند، باید با این نام مورد جستجو قرار گیرد. بنابراین، این جایی است که لیست viewResolvers به کار میآید:
for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } }
این لیستی از نمونههایی ViewResolver است، که شامل ThymeleafViewResolver میباشد که توسط کتابخانه thymeleaf-spring5 ارائه شده است. این resolver میداند که در آن برای مشاهده نمایشها چه اقداماتی را انجام دهد و نمونههای نمایش مربوطه را فراهم کند.
پس از فراخوانی متد رندر View، در نهایت Spring درخواست پردازش را با ارسال صفحه HTML به مرورگر کاربر، تکمیل میکند.
پشتیبانی از REST :
فراتر از سناریو MVC معمولی، ما همچنین میتوانیم از این چارچوب برای ایجاد وب سرویس REST استفاده کنیم.
به سادگی میتوانید یک منبع را به عنوان ورودی بپذیرید، یک POJO را به عنوان یک آرگومان متد مشخص کنید و آن را با @RequestBody حاشیهنویسی کنید. شما همچنین میتوانید تابع خود را با @RequestBody حاشیهنویسی کنید تا مشخص کنید که نتیجه آن باید مستقیما به یک پاسخ HTTP تبدیل شود:
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; @ResponseBody @PostMapping("/message") public MyOutputResource sendMessage( @RequestBody MyInputResource inputResource) { return new MyOutputResource("Received: " + inputResource.getRequestMessage()); }
برای جمع کردن DTOهای داخلی به فرم REST، چارچوب از زیرساخت HttpMessageConverter استفاده میکند. به عنوان مثال، یکی از پیادهسازیها MappingJackson2HttpMessageConverter است که قادر به تبدیل اشیاء به/از JSON با استفاده از کتابخانه جکسون است.
به منظور سادهسازی ایجاد یک API REST ساده، حاشیهنویسی @RestController توسط Spring معرفی میشود.
import org.springframework.web.bind.annotation.RestController; @RestController public class RestfulWebServiceController { @GetMapping("/message") public MyOutputResource getMessage() { return new MyOutputResource("Hello!"); } }
نتیجه
در این مقاله، جزئیات پردازش یک درخواست در چارچوب MVC Spring بررسی گردید. شما دیدید که چگونه بخشهای مختلف چارچوب با یکدیگر همکاری میکنند تا تمامی این امکانات را فراهم آورند.