深入解讀 Spring MVC:Web 開發的得力助手
在當今軟件開發的廣袤領域中,Web 應用的構建至關重要。而在眾多優秀的框架中,Spring MVC 猶如一顆璀璨的明星,閃耀著獨特的光芒。Spring MVC 作為一種強大而靈活的框架,為開發者提供了一套完善的解決方案,用于構建高效、可擴展且易于維護的 Web 應用程序。
當我們踏上探索 Spring MVC 的旅程,就仿佛打開了一扇通往精彩編程世界的大門。它以其簡潔明了的架構設計、豐富多樣的功能特性,成為了無數開發者的首選。無論是處理復雜的業務邏輯,還是實現流暢的用戶交互,Spring MVC 都展現出卓越的能力。
在接下來的篇章中,我們將深入剖析 Spring MVC 的各個方面,從其基本概念到核心組件,從請求處理到視圖呈現,一步步揭開它神秘的面紗,領略它在 Web 開發領域所蘊含的巨大潛力和價值。讓我們一同開啟這場關于 Spring MVC 的精彩探索之旅,去發現它如何為我們的 Web 開發之路注入強大動力。
詳解Spring MVC
1.MVC的概念
在講解Spring MVC前,我們可以先了解一下MVC的概念,MVC大多數的說法是一種軟件設計架構,其構成為:
- 控制器(Controller):是模型和視圖連接的橋梁,負責分發調度用戶請求交由響應的Model的處理,并將結果交由視圖進行渲染。
- 模型(Model):模型負責業務邏輯和數據處理,包含數據庫訪問、邏輯運算等工作。
- 視圖(View):負責渲染頁面請求,呈現給用戶的界面。
2. Spring MVC核心組件有哪些?
從整體來說大概有下面這幾個吧:
- DispatcherServlet :負責接收分發用戶請求,并給予客戶端響應。
- HandlerMapping :根據前端發送的映射找到合適Handler 。
- HandlerAdapter :根據前者找到的Handler,適配對應的Handler。
- Handler :處理用戶的請求。
- ViewResolver :視圖解析器,根據Handler 返回結果,解析并渲染成真正的視圖,傳遞給DispatcherServlet 返回給前端。
組件的時候我們就大概已經把流程給說了,當用戶請求到達我們的應用時:
- 通過DispatcherServlet到HandlerMapping 確定控制器controller。
- 控制器將進行邏輯處理并將信息即model(注意這里的model不是mvc概念的model,而單指數據)和視圖名稱返回的DispatcherServlet。
- DispatcherServlet通過視圖解析器ViewResolver匹配到視圖。
- DispatcherServlet將模型交付給視圖完成數據渲染并呈現給用戶。
3. Spring MVC如何進行統一異常處理
我們一般會使用注解的方式ControllerAdvice+ExceptionHandler注解組合,示例代碼如下:
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(BaseException.class)
public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
//......
}
@ExceptionHandler(value = ResourceNotFoundException.class)
public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
//......
}
}
4. DispatcherServlet處理請求的過程
和上述流程圖解流程差不多,我們這里通過源碼走讀的方式進行展開:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
//如果是get請求就調用doGet
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
......
}
}
........
}
然后DispatcherServlet的doDispatch就會找到合適的mapping交由適配器找到合適的handler進行包裝然后著手處理:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//這里會通過mapping找到合適的handler
mappedHandler = this.getHandler(processedRequest);
//......
//適配器是適配執行對應的 Handler
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
//調用處理器的handle得到上述所說的視圖名view和模型數據model,該調用內部會走到請求映射對應的controller上
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
//設置視圖名稱
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} .......
}
5. 過濾器和攔截器有什么區別?(重點)
我們不妨基于一段示例代碼來了解一下過濾器和攔截器的區別,首先我們在spring boot的web項目中添加一個過濾器
@Component
public class MyFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(MyFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("Filter 前置處理");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
logger.info("Filter 處理中");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
logger.info("Filter 后置處理");
}
}
然后再添加一個攔截器:
@Component
public class MyInterceptor implements HandlerInterceptor {
private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("Interceptor 前置");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info("Interceptor 處理中");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.info("Interceptor 后置");
}
}
編寫好攔截器之后,我們需要基于一段配置使得攔截器可以攔截所有url
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}
完成過濾器和攔截器的編寫,我們不妨編寫一個controller并進行啟動測試:
@RestController
public class TestController {
private static Logger logger = LoggerFactory.getLogger(TestController.class);
@GetMapping("hello")
public void hello() {
logger.info("TestController執行了hello方法");
}
}
完成代碼編寫后鍵入下面這條命令
curl 127.0.0.1:8080/hello
可以看到下面這樣一段輸出結果,就說明過濾器和攔截器都生效了
2023-02-14 19:50:14.401 INFO 31904 --- [nio-8080-exec-1] com.example.demo.MyFilter : Filter 處理中
2023-02-14 19:50:14.412 INFO 31904 --- [nio-8080-exec-1] com.example.demo.MyInterceptor : Interceptor 前置
2023-02-14 19:50:14.420 INFO 31904 --- [nio-8080-exec-1] com.example.demo.TestController : TestController執行了hello方法
2023-02-14 19:50:14.451 INFO 31904 --- [nio-8080-exec-1] com.example.demo.MyInterceptor : Interceptor 處理中
2023-02-14 19:50:14.452 INFO 31904 --- [nio-8080-exec-1] com.example.demo.MyInterceptor : Interceptor 后置
6. 工作原理不同
過濾器的工作原理就是將一個個過濾器組裝成一條鏈,以責任鏈模式的方式,在請求到達web容器時,按照順序依次執行一個個filter,如下圖所示,當我們的請求TestController的hello方法時,請求就會依次從spring mvc自帶的調用鏈走到我們自定義的myFilter。
而攔截器則時基于動態代理的方式實現的,感興趣的讀者可以自行了解AOP的工作機制。
7. 應用范圍的區別
從源碼中我們可以看到過濾器是在tomcat相關的包下面,很明顯它只能作用于web容器中。
而攔截器是屬于spring mvc的包下,這也就意味著他的作用范圍還可以是application或者swing等程序。
8. 執行順序不同
我們上文請求輸出了下面這樣一段結果
2023-02-14 21:37:04.332 INFO 53236 --- [nio-8080-exec-1] com.example.demo.MyFilter : Filter 處理中
2023-02-14 21:56:38.812 INFO 53236 --- [nio-8080-exec-1] com.example.demo.MyInterceptor : Interceptor 前置
2023-02-14 21:56:38.826 INFO 53236 --- [nio-8080-exec-1] com.example.demo.TestController : TestController執行了hello方法
2023-02-14 21:56:38.871 INFO 53236 --- [nio-8080-exec-1] com.example.demo.MyInterceptor : Interceptor 處理中
2023-02-14 21:56:38.871 INFO 53236 --- [nio-8080-exec-1] com.example.demo.MyInterceptor : Interceptor 后置
可以看出一個web請求優先經過tomcat的過濾器,然后在到達spring的攔截器,他們的執行順序如下圖所示:
9. 注入bean的方式不同
為了了解過濾器和攔截器注入bean的差異,我們編寫一個測試bean
@Component
public class TestBean {
private static Logger logger = LoggerFactory.getLogger(TestBean.class);
public void hello(){
logger.info("測試bean輸出hello");
}
}
我們基于上述代碼往過濾器和攔截器中分別注入bean,首先是過濾器的代碼示例
@Component
public class MyFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(MyFilter.class);
@Autowired
private TestBean bean;
......
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
logger.info("Filter 處理中");
bean.hello();
filterChain.doFilter(servletRequest, servletResponse);
}
......
}
然后是攔截器的代碼示例
@Component
public class MyInterceptor implements HandlerInterceptor {
private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
@Autowired
private TestBean bean;
....
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
bean.hello();
logger.info("Interceptor 處理中");
}
.....
}
再次啟動測試時發現,過濾器正常執行,攔截器注入的bean報了空指針,原因很簡單,過濾器是在spring context加載完成之前加載的,所以在它創建時,我們自定義的bean還沒有生成。
解決方式也很簡單,在加載MyMvcConfig 時,手動創建getMyInterceptor的@Bean方法,讓TestBean在spring context加載之前就IOC到容器中:
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Bean
public MyInterceptor getMyInterceptor(){
return new MyInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");
}
}
10. 調整順序的方式不同
過濾器直接在類上使用@Order(數字)注解即可調整順序,值越小越早執行。而攔截器則是在addInterceptors方法中使用order方法調整順序。
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**").order(1);
}
需要了解的是多個攔截器,最先執行的攔截器postHandle反而最后執行。對此我們不妨做個實驗,首先編寫一個攔截器2:
@Component
public class MyInterceptor2 implements HandlerInterceptor {
private static Logger logger = LoggerFactory.getLogger(MyInterceptor2.class);
@Autowired
private TestBean bean;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("Interceptor2 前置");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
bean.hello();
logger.info("Interceptor2 處理中");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.info("Interceptor2 后置");
}
}
然后注冊到容器中:
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Bean
public MyInterceptor getMyInterceptor(){
return new MyInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**").order(1);
registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**").order(2);
}
}
輸出結果如下,可以看攔截器1優先級最高,最先執行preHandle、postHandle,反而afterCompletion最后執行。
這一點我們可以在源碼中找到答案,我們可以在DispatcherServlet的doDispatch方法中看到答案,核心代碼如下,從筆者注釋中可以看到applyPreHandle就是spring mvc執行preHandle的地方,我們點入查看邏輯可以看到它的for循環是正序的,這也就意味著攔截器的preHandle方法是順序執行的,其他兩個方法同理,不多贅述。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//preHandle都是正向for循環依次執行
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
......
applyDefaultViewName(processedRequest, mv);
//postHandle也都是正向for循環依次執行
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
.........
......
catch (Throwable err) {
//攔截器的afterCompletion倒敘for循環執行
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
.......
}
小結
通過對 Spring MVC 的深入解讀,我們清晰地認識到它作為 Web 開發得力助手的重要地位和強大功能。Spring MVC 憑借其完善的架構和豐富的特性,為開發者提供了高效便捷的開發體驗。它簡化了 Web 應用的構建過程,在請求處理、視圖渲染等方面展現出卓越的性能和靈活性。
通過對其核心概念和工作流程的剖析,我們理解了如何更好地利用這一框架來構建高質量、可擴展的 Web 應用。無論是新手開發者還是經驗豐富的專業人士,都能從 Spring MVC 中受益,借助它實現更出色的 Web 項目開發成果。