深入解读 Spring MVC:Web 开发的得力助手

开发
我们将深入剖析 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 项目开发成果。

责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2024-02-06 09:53:45

Pythonget()函数Dictionary

2010-04-16 10:49:38

2012-06-04 15:38:34

台式机评测

2011-12-19 14:24:28

惠普台式机商用

2012-06-14 11:14:26

MetroGridHe

2009-09-18 20:36:32

视频会议系统视频输入设备红杉树

2023-11-21 09:11:31

2023-05-31 08:37:06

Java并发编程

2011-04-12 09:40:23

日立JP保险业IT

2019-07-04 09:00:00

Web控制器架构

2024-06-17 08:45:00

2009-06-22 11:54:28

Spring MVCSpringframe

2013-08-13 09:56:00

PythonWeb2py

2024-04-11 13:51:47

markdown前端

2013-12-17 15:12:13

2022-01-21 08:02:04

开发

2023-11-02 18:01:24

SpringMVC配置
点赞
收藏

51CTO技术栈公众号