1. 引言
簡單來説,在 前端控制器設計模式中,單個控制器負責將傳入的 HTTP 請求引導到應用程序中的所有其他控制器和處理器。
Spring 的 DispatcherServlet 實現了該模式,因此負責正確地協調 HTTP 請求到其正確的處理器。
在本文中,我們將 分析 Spring DispatcherServlet 的請求處理工作流程 以及如何實現參與該工作流程的多個接口。
2. DispatcherServlet 請求處理
本質上,DispatcherServlet 處理傳入的 HttpRequest,並委託請求,以及根據 Spring 應用中配置的 HandlerAdapter 接口來處理該請求,同時伴隨的註解指定了處理器、控制器端點和響應對象。
讓我們更深入地瞭解 DispatcherServlet 如何處理一個組件:
- 與 DispatcherServlet 關聯的 WebApplicationContext,通過鍵 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 搜索並提供給流程中的所有元素
- DispatcherServlet 查找配置在你的 dispatcher 中所有 HandlerAdapter 接口的實現——每個找到並配置的實現通過 handle() 處理請求,貫穿整個流程
- LocaleResolver 可選地綁定到請求,以便流程中的元素可以解析區域設置
- ThemeResolver 可選地綁定到請求,允許元素(如視圖)確定使用哪個主題
- 如果指定了 MultipartResolver,則檢查請求是否存在 MultipartFiles,任何找到的都包裝在 MultipartHttpServletRequest 中進行進一步處理
- 在 WebApplicationContext 中聲明的 HandlerExceptionResolver 實現會捕獲在請求處理過程中拋出的異常
你可以在這裏瞭解如何註冊和設置 DispatcherServlet 的所有方法。
3. HandlerAdapter 接口
<em>HandlerAdapter</em> 接口使得通過多個特定接口,控制器、Servlet、<em>HttpRequests</em> 和 HTTP 路徑得以使用。<em>HandlerAdapter</em> 接口因此在 <em>DispatcherServlet</em> 請求處理流程的許多階段中發揮着關鍵作用。
首先,每個 <em>HandlerAdapter</em> 實現都會被放置到你的 Dispatcher 的 <em>getHandler()</em> 方法中。然後,這些實現會 <em>handle()</em> <em>HttpServletRequest</em> 對象,隨着執行鏈的推進。
在後面的部分,我們將更詳細地探討一些最重要的和常用的 <em>HandlerAdapters</em>。
3.1. 映射關係
為了理解映射關係,我們首先需要了解如何註解控制器,因為控制器對於 <em >HandlerMapping</em> 接口至關重要。
<em >SimpleControllerHandlerAdapter</em> 允許在不使用 <em >@Controller</em> 註解的情況下明確實現控制器。
<em >RequestMappingHandlerAdapter</em> 支持使用帶有 <em >@RequestMapping</em> 註解的方法。
我們將重點關注 <em >@Controller</em> 註解,但一個包含多個示例,並使用 <em >SimpleControllerHandlerAdapter</em> 的有用的資源也可用。
<em >@RequestMapping</em> 註解定義了處理程序將在與它關聯的 <em >WebApplicationContext</em> 中提供的特定端點。
讓我們來看一個暴露並處理 <em >/user/example</em> 端點的 <em >Controller</em> 的示例:
@Controller
@RequestMapping("/user")
@ResponseBody
public class UserController {
@GetMapping("/example")
public User fetchUserExample() {
// ...
}
}由 @RequestMapping 註解指定的路徑,通過 HandlerMapping 接口進行內部管理。
URL 結構與 DispatcherServlet 本身相關聯,並由 Servlet 映射確定。
因此,如果 DispatcherServlet 被映射到 ‘/’,則所有映射都將覆蓋該映射。
但是,如果 Servlet 映射為 ‘/dispatcher‘,則所有 @RequestMapping 註解都將相對於該根 URL 進行處理。
請記住,‘/’ 與 ‘/*’ 是不一樣的! 對於 Servlet 映射,‘/’ 是默認映射,並暴露所有 URL 到 Dispatcher 的責任區域。
‘/*’ 對許多新開發的 Spring 開發者來説都比較困惑。它並不能指定具有相同 URL 上下文的所有路徑都位於 Dispatcher 的責任區域下。相反,它會覆蓋並忽略其他 Dispatcher 映射。因此,‘/example’ 將返回 404 錯誤!
因此,不應該在非常有限的情況下使用 ‘/*’(例如,配置過濾器)。
3.2. HTTP 請求處理
DispatcherServlet 的核心職責是分發接收到的 HttpRequests 到通過 @Controller 或 @RestController 註解指定的正確處理程序。
順便説明的是,@Controller 和 @RestController 之間的主要區別在於響應生成方式——@RestController 默認定義了 @ResponseBody。
關於 Spring 控制器更深入的説明,請參閲此處。
3.3. <em>ViewResolver</em> 接口
<em>ViewResolver</em> 是作為 <em>ApplicationContext</em> 對象上的配置設置,附加到 <em>DispatcherServlet</em> 上的。
<em>ViewResolver</em> 確定了分發器所提供的視圖類型以及它們是從何處提供的。
以下是一個我們將放置在我們的 <i >AppConfig</i> 中用於渲染 JSP 頁面的示例:
@Configuration
@EnableWebMvc
@ComponentScan("com.baeldung.springdispatcherservlet")
public class AppConfig implements WebMvcConfigurer {
@Bean
public UrlBasedViewResolver viewResolver() {
UrlBasedViewResolver resolver
= new UrlBasedViewResolver();
resolver.setPrefix("/WEB-INF/view/");
resolver.setSuffix(".jsp");
resolver.setViewClass(JstlView.class);
return resolver;
}
}非常直觀! 這主要分為三個部分:
- 設置前綴,這會設置默認 URL 路徑以查找已設置的視圖
- 默認視圖類型,通過後綴設置
- 在解析器上設置視圖類,允許諸如 JSTL 或 Tiles 之類的技術與渲染視圖相關聯
一個常見的問題是調度器 ViewResolver 和整個項目目錄結構之間的關係如何。 讓我們先了解一下基本內容。
以下是使用 Spring 的 XML 配置的 InternalViewResolver 的示例路徑配置:
<property name="prefix" value="/jsp/"/>為了便於我們舉例説明,我們假設我們的應用程序正在託管在:
http://localhost:8080/這是本地託管的 Apache Tomcat 服務器的默認地址和端口。
假設我們的應用程序名為 dispatcherexample-1.0.0,我們的 JSP 視圖將可以通過以下地址訪問:
http://localhost:8080/dispatcherexample-1.0.0/jsp/在普通 Spring 項目中使用 Maven 的這些視圖的路徑如下:
src -|
main -|
java
resources
webapp -|
jsp
WEB-INF默認的視圖位置位於 WEB-INF 目錄下。上述片段中 InternalViewResolver 指定的路徑決定了您的視圖將在 ‘src/main/webapp’ 目錄下的哪個子目錄中可用。
3.4. <em>LocaleResolver</em> 接口
通過 LocaleResolver 接口,我們可以自定義 dispatcher 的會話、請求或 Cookie 信息。
CookieLocaleResolver 是一個實現,允許使用 Cookie 配置無狀態應用程序屬性。讓我們將其添加到 AppConfig。
@Bean
public CookieLocaleResolver cookieLocaleResolverExample() {
CookieLocaleResolver localeResolver
= new CookieLocaleResolver();
localeResolver.setDefaultLocale(Locale.ENGLISH);
localeResolver.setCookieName("locale-cookie-resolver-example");
localeResolver.setCookieMaxAge(3600);
return localeResolver;
}
@Bean
public LocaleResolver sessionLocaleResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
localResolver.setDefaultTimeZone(TimeZone.getTimeZone("UTC"));
return localeResolver;
}
SessionLocaleResolver 允許在狀態化應用程序中進行會話特定的配置。
setDefaultLocale 方法代表地理、政治或文化區域,而 setDefaultTimeZone(時區) 則確定應用程序中特定 時區 Bean 的相關 時區 對象。時區
這兩個方法均可在上述所有 LocaleResolver 實現中找到。
3.5. <em>ThemeResolver</em> 接口
Spring 提供視圖樣式主題功能。
讓我們看看如何配置 Dispatcher 以處理主題。
首先,讓我們設置所有必要的配置,以便查找和使用我們的靜態主題文件。 我們需要為我們的 ThemeSource 設置靜態資源位置,以便配置實際的 Themes(Theme 對象包含來自這些文件中規定的所有配置信息)。 將以下內容添加到 AppConfig:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/", "/resources/")
.setCachePeriod(3600)
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
@Bean
public ResourceBundleThemeSource themeSource() {
ResourceBundleThemeSource themeSource
= new ResourceBundleThemeSource();
themeSource.setDefaultEncoding("UTF-8");
themeSource.setBasenamePrefix("themes.");
return themeSource;
}
由 DispatcherServlet 管理的請求可以通過傳遞到 ThemeChangeInterceptor 對象的 setParamName() 方法中指定的參數來修改主題。在 AppConfig 中添加:
@Bean
public CookieThemeResolver themeResolver() {
CookieThemeResolver resolver = new CookieThemeResolver();
resolver.setDefaultThemeName("example");
resolver.setCookieName("example-theme-cookie");
return resolver;
}
@Bean
public ThemeChangeInterceptor themeChangeInterceptor() {
ThemeChangeInterceptor interceptor
= new ThemeChangeInterceptor();
interceptor.setParamName("theme");
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(themeChangeInterceptor());
}
以下 JSP 標籤已添加到我們的視圖中,以確保正確的樣式顯示:
<link rel="stylesheet" href="${ctx}/<spring:theme code='styleSheet'/>" type="text/css"/>以下 URL 請求使用“theme”參數通過我們配置的 ThemeChangeIntercepter 渲染 example 主題:
http://localhost:8080/dispatcherexample-1.0.0/?theme=example3.6. <em>MultipartResolver</em> 接口
<em>MultipartConfigElement</em> 配置用於簡單地配置 <em>MultipartHttpServletRequest</em> 的最大大小,以便在其他元素處理過程中進一步使用,如果找到多部分內容。添加到 <em>AppConfig</em> 中:
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofBytes(10000000L));
factory.setMaxRequestSize(DataSize.ofBytes(10000000L));
return factory.createMultipartConfig();
}
現在我們已經配置了 MultipartResolver Bean,接下來我們將設置一個控制器來處理 MultipartFile 請求:
@Controller
public class MultipartController {
@Autowired
ServletContext context;
@PostMapping("/upload")
public ModelAndView FileuploadController(
@RequestParam("file") MultipartFile file)
throws IOException {
ModelAndView modelAndView = new ModelAndView("index");
InputStream in = file.getInputStream();
String path = new File(".").getAbsolutePath();
FileOutputStream f = new FileOutputStream(
path.substring(0, path.length()-1)
+ "/uploads/" + file.getOriginalFilename());
int ch;
while ((ch = in.read()) != -1) {
f.write(ch);
}
f.flush();
f.close();
in.close();
modelAndView.getModel()
.put("message", "File uploaded successfully!");
return modelAndView;
}
}我們可以使用標準形式向指定端點提交文件。上傳的文件將在 ‘CATALINA_HOME/bin/uploads’ 目錄下可用。
3.7. <em>HandlerExceptionResolver</em> 接口
Spring 的 <em>HandlerExceptionResolver</em> 提供了一種統一的錯誤處理機制,適用於整個 Web 應用程序、單個控制器或一組控制器。
為了提供應用程序級別的自定義異常處理,請創建一個標註了 <em>@ControllerAdvice</em> 註解的類。
@ControllerAdvice
public class ExampleGlobalExceptionHandler {
@ExceptionHandler
@ResponseBody
public String handleExampleException(Exception e) {
// ...
}
}該類中標記了 @ExceptionHandler 註解的所有方法將在調度器負責區域內的每個控制器中可用。
HandlerExceptionResolver 接口的實現,在 DispatcherServlet 的 ApplicationContext 中可用,以便 攔截特定的控制器,在調度器負責區域內 當使用 @ExceptionHandler 註解時,並正確地將類作為參數傳遞:
@Controller
public class FooController{
@ExceptionHandler({ CustomException1.class, CustomException2.class })
public void handleException() {
// ...
}
// ...
}handleException() 方法現在將作為 FooController 在我們上面的示例中用於處理異常,如果發生CustomException1 或 CustomException2 異常。
以下是一篇文章深入探討了 Spring Web 應用程序中異常處理的詳細信息。