SpringBoot Spring MVC
作者:mmseoamin日期:2023-12-20

一、初始Spring MVC

1.1 什么是Spring MVC

  • 1、Spring MVC是Spring框架的一部分
      Spring MVC是一种轻量级的、基于MVC的Web层应用框架。Spring MVC通过把模型-视图-控制器分离,将Web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。

    • 2、Spring MVC是一种分层设计思想
        目标是将复杂的应用系统按照分层处理进行设计和规划,通过这种设计方式达到分而治之的效果,从而降低程序开发的难度,提高代码的可维护性。
        MVC是模型(Model)、视图(View)、控制器(Controller)的简写,就是将业务逻辑、数据、显示分离的方法来组织代码。MVC主要作用是降低了视图与业务逻辑间的双向耦合。
        Model(数据模型),提供要展示的数据,因此包含数据和行为,现在一般都分离开来:数据DAO和服务层(行为Service)。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。可简单理解为和数据库打交道的行为和交互的数据。
        View(视图),负责进行模型的展示,一般就是我们见到的用户界面,客户想看到的东西。
        Controller(控制器),接收用户请求,委托给模型进行处理,处理完毕后把返回的模型数据返回给视图,由视图负责展示。 也就是说控制器做了个调度员的工作。
        图示:

      SpringBoot Spring MVC,第1张

      1.2 Spring MVC的优缺点

      1.2.1 优点
      • 1、清晰的角色划分
          控制器(controller)、验证器(validator)、命令对象(command obect)、表单对象(form object)、模型对象(model object)、Servlet分发器(DispatcherServlet)、处理器映射(handler mapping)、试图解析器(view resoler)等等。每一个角色都可以由一个专门的对象来实现。

        • 2、强大而直接的配置方式
            将框架类和应用程序类都能作为JavaBean配置,支持跨多个context的引用。

          • 3、可重用的业务代码
              可以使用现有的业务对象作为命令或表单对象,而不需要去扩展某个特定框架的基类。

            • 4、Spring Bean的生命周期
                可以被限制在当前的HTTP Request或者HTTP Session。准确的说,这并非Spring MVC框架本身特性,而应归属于Spring MVC使用的WebApplicationContext容器。

              1.2.2 缺点
              • 1、没有明确的定义
                  完全理解MVC并不是很容易。使用MVC需要精心的计划,由于它的内部原理比较复杂,所以需要花费一些时间去思考。同时由于模型和视图要严格的分离,这样也给调试应用程序带来了一定的困难。每个构件在使用之前都需要经过彻底的测试。

                • 2、不适合小型,中等规模的应用程序
                    花费大量时间将MVC应用到规模并不是很大的应用程序通常会得不偿失。

                  • 3、增加系统结构和实现的复杂性
                      对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。

                    • 4、视图与控制器间的过于紧密的连接
                        视图与控制器是相互分离,但却是联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。

                      • 5、视图对模型数据的低效率访问
                          依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。

                        二、Spring MVC工作原理

                        2.1 MVC的工作流程

                          图示:

                        SpringBoot Spring MVC,第2张

                          或者:

                        SpringBoot Spring MVC,第3张

                          详细步骤:

                        • 1、用户发送请求至前端控制器DispatcherServlet;

                          • 2、DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handler;

                            • 3、处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;

                              • 4、DispatcherServlet调用HandlerAdapter处理器适配器;

                                • 5、HandlerAdapter经过适配调用具体处理器(Handler,也叫后端控制器);

                                  • 6、Handler执行完成返回ModelAndView;

                                    • 7、HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;

                                      • 8、DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;

                                        • 9、ViewResolver解析后返回具体View;

                                          • 10、DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)

                                            • 11、DispatcherServlet响应用户。

                                              2.2 Spring MVC的主要组件

                                                SpringMVC中的Servlet一共有三个层次,分别是HttpServletBean、FrameworkServlet和 DispatcherServlet:

                                              HttpServletBean直接继承自Java的HttpServlet,其作用是将Servlet中配置的参数设置到相应的属性;

                                              FrameworkServlet初始化了WebApplicationContext;

                                              DispatcherServlet初始化了自身的9个组件。

                                                Handler,也就是处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler。

                                                最常见的主要组件有六个:

                                              • 1、前端控制器DispatcherServlet(框架提供)
                                                  DispatcherServlet是Spring MVC的入口函数。接收请求,响应结果,相当于转发器,中央处理器。有了DispatcherServlet ,可以大大减少其它组件之间的耦合度。
                                                  用户请求到达前端控制器,就相当于MVC模式中的C,DispatcherServlet是整个流程控制的中心,由它调用其它组件来处理用户的请求。

                                                • 2、处理器映射器HandlerMapping (框架提供)
                                                    HandlerMapping 负责根据用户请求(URL),找到相应的Handler即处理器(Controller),SpringMVC提供了不同映射器实现的不同映射方式,例如:配置文件方式,实现接口方式,注解方式等。

                                                  publicinterfaceHandlerMapping{HandlerExecutionChaingetHandler(HttpServletRequest request)throwsException;}

                                                    HandlerMapping接口中只定义了一个方法,就是通过request找到HandlerExecutionChain,而HandlerExecutionChain包装了一个Handler和一组Interceptors。

                                                  • 3、处理器适配器HandlerAdapter (框架提供)
                                                      按照特定规则(HandlerAdapter要求的规则)去执行Handler,通过 HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行处理。

                                                    publicinterfaceHandlerAdapter{//判断是否支持传入的handlerbooleansupports(Object handler);//使用给定的handler处理请求ModelAndViewhandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException;//返回上次修改时间,可以返回-1表示不支持longgetLastModified(HttpServletRequest request,Object handler);}

                                                      从名字上看,它就是一个适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。任意形式的Handler通过使用适配器,可以“转换”成固定形式,然后交给Servlet来处理。每种Handler都要有对应的HandlerAdapter才能处理请求。

                                                    • 4、处理器Handler (需要开发)
                                                        Handler是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下,Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求,所以一般情况下需要工程师根据业务需求来开发Handler。

                                                      • 5、视图解析器View Resolver(框架提供)
                                                          作用:进行视图解析,根据逻辑视图名解析成真正的视图(View),View Resolver 负责将处理结果生成View视图。首先,根据逻辑视图名解析成物理视图名(即具体的页面地址),再生成View视图对象,最后对View进行渲染,将处理结果通过页面展示给用户。

                                                        publicinterfaceViewResolver{ViewresolveViewName(String viewName,Locale locale)throwsException;}

                                                          ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。

                                                        • 6、视图View (需要开发)
                                                            View是一个接口,实现类才可以支持不同的View类型。

                                                            总结:处理器Handler(也就是平常说的Controller控制器)以及视图层View ,都是需要自行开发的。其他的一些组件,如:前端控制器DispatcherServlet、处理器映射器HandlerMapping、处理器适配器HandlerAdapter等都是由框架提供。

                                                            除了上面的这些,还有一些相对不那么常用的组件(了解即可):

                                                          • HandlerExceptionResolver
                                                              其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后怎么办呢?这就需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。HandlerExceptionResolver的接口定义:

                                                            publicinterfaceHandlerExceptionResolver{ModelAndViewresolveException(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex);}

                                                              从上面的分析中我们可以知道HandlerExceptionResolver只能处理页面渲染之前的异常,页面渲染过程中的异常,它是不能处理的,这时可以让容器跳转到指定的错误页面来处理异常。

                                                            • ThemeResolver
                                                                用于解析主题。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。

                                                              • MultipartResolver
                                                                  用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。

                                                                2.3 DispatcherServlet的作用

                                                                  SpringMVC的核心就是DispatcherServlet,DispatcherServlet实质也是一个HttpServlet。DispatcherSevlet负责将请求分发,所有的请求都有经过它来统一分发。大致看下SpringMVC请求处理的流程:

                                                                SpringBoot Spring MVC,第4张

                                                                  用户向服务器发送请求,请求会到DispatcherServlet,DispatcherServlet 对请求URL进行解析,得到请求资源标识符(URI),然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括一个Handler处理器对象、多个HandlerInterceptor拦截器对象),最后以HandlerExecutionChain对象的形式返回。

                                                                  DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。

                                                                  在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

                                                                  HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息

                                                                  数据转换:对请求消息进行数据转换。如String转换成Integer、Double等

                                                                  数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等

                                                                  数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中

                                                                  Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象;根据返回的ModelAndView,选择一个适合的ViewResolver返回给DispatcherServlet;ViewResolver 结合Model和View,来渲染视图,最后将渲染结果返回给客户端。

                                                                三、Spring MVC注解

                                                                3.1 注解原理

                                                                  注解属于Java语言的特性,是在Java5.0引入的新特性 。

                                                                  注解是用于给Java代码附加元数据,可在编译时或运行时解析并处理这些元数据。Java代码可以是包名、类、方法、成员变量、参数等,且附加的元数据不会影响源代码的执行。

                                                                  可以这样理解Java注解:想像Java代码如包名、类、方法、成员变量、参数等都是具有生命,注解就是给代码中某些元素贴上去的一张标签。通俗点来讲,注解如同一张标签。

                                                                3.1.1 注解的基本语法
                                                                • 1、注解的定义
                                                                    定义的格式:

                                                                  [@Target][@Retention][@Documented][@Inherited]public@interface[名称]{// 元素}

                                                                    形式跟接口很类似,不过前面多了一个@符号。

                                                                    定义注解时可给注解添加属性,也叫注解的成员变量。注解只有成员变量,没有方法。注解的成员变量的以“无形参的方法”形式来声明。

                                                                  @Target({ElementType.FIELD,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceTest{Stringvalue();}

                                                                    还可给注解的属性设定默认值:

                                                                  @Target({ElementType.FIELD,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceTest{Stringvalue()default"hello";}

                                                                    注解的属性可支持数据类型有如下:

                                                                  所有基本数据类型(int,float,boolean,byte,double,char,long,short)

                                                                  String类型

                                                                  Class类型

                                                                  enum类型

                                                                  Annotation类型

                                                                  以上所有类型的数组

                                                                  • 2、内置注解
                                                                      JDK5.0加入了下面三个内置注解:

                                                                    @Override:表示当前的方法定义将覆盖父类中的方法;

                                                                    @Deprecated:表示代码被弃用,如果使用了被@Deprecated注解的代码则编译器将发出警告;

                                                                    @SuppressWarnings:表示关闭编译器警告信息。

                                                                    • 3、元注解
                                                                        元注解的作用就是负责注解其他注解。或者说元注解是一种基本注解,但是它能够应用到其它注解上面。
                                                                        如果难于理解的话,可这样理解:元注解也是一张标签,但它是一张特殊的标签,它的作用就是给其他普通的标签进行解释说明的。

                                                                      元注解

                                                                      作用

                                                                      备注

                                                                      @Target

                                                                      指定了注解运用的地方

                                                                      你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。

                                                                      @Retention

                                                                      说明注解的存活时间

                                                                      我们可以这样的方式来加深理解,@Retention 去给一张标签解释的时候,它指定了这张标签张贴的时间。@Retention 相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。

                                                                      @Document

                                                                      说明注解是否能被文档化

                                                                      不常用

                                                                      @Inhrited

                                                                      说明注解能否被继承

                                                                      不常用

                                                                      显示详细信息

                                                                      3.1.2 注解的处理

                                                                        可以在两个时期对注解进行处理:编译时和运行时。

                                                                      • 1、运行时处理
                                                                          当@Retention的值设定为RetentionPolicy.RUNTIME注解信息会存在.class文件中,通知单JVM加载.class文件时会把注解也加载到JVM中,所以就可在运行时获取注解的信息。
                                                                          运行时处理:运行时处理是通过反射机制获取注解。
                                                                          运行时处理的缺点:

                                                                        通过反射会影响运行效率;

                                                                        如果注解无法保存到运行时的话,是无法使用运行时处理的。

                                                                        • 2、编译时处理
                                                                            编译时处理需要使用到APT技术,该技术提供了一套编译期的注解处理流程。

                                                                          SpringBoot Spring MVC,第5张

                                                                            在编译期扫描.java文件的注解,并传递到注解处理器,注解处理器可根据注解生成新的.java文件,这些新的.java问和原来的.java一起被javac编译。

                                                                          SpringBoot Spring MVC,第6张

                                                                            这里需要引入注解处理器这个概念,注解处理器是一个在javac编译期处理注解的工具,你可以创建注解处理器并注册,在编译期你创建的处理器以Java代码作为输入,生成文件.java文件作为输出。

                                                                            注意:注解处理器不能修改已经存在的Java类(即不能向已有的类中添加方法)。只能生成新的Java类。下面定义注解处理器的四个重要的方法:

                                                                          publicclassCustomProcessorextendsAbstractProcessor{//初始化注解处理器//@param processingEnv APT框架的环境对象,通过该对象可以获取到很多工具类@Overridepublicsynchronizedvoidinit(ProcessingEnvironment processingEnv){super.init(processingEnv);}//配置该注解处理器需要处理的注解类型@OverridepublicSetgetSupportedAnnotationTypes(){returnsuper.getSupportedAnnotationTypes();}//配置该注解处理器支持的最新版本@OverridepublicSourceVersiongetSupportedSourceVersion(){returnsuper.getSupportedSourceVersion();}//用于处理注解,并生成.java文件@Overridepublicbooleanprocess(Set annotations,RoundEnvironment roundEnv){returnfalse;}}

                                                                            编译时处理的优点: 不在运行时进行操作,所以对程序的性能不会有什么影响。

                                                                            编译时处理的缺点:

                                                                          无法对原来的.java文件进行修改;

                                                                          生成额外的.java文件;

                                                                          因为是在编译期进行处理注解,所以会对编译速度有影响。

                                                                          3.2 @PathVariable和@RequestParam的区别

                                                                            @RequestParam与@PathVariable为Spring的注解,都可以用于在Controller层接收前端传递的数据,不过两者的应用场景不同。

                                                                            @PathVariable主要用于接收http://host:port/path/{参数值}类型的数据。@RequestParam主要用于接收http://host:port/path?参数名=参数值类型的数据,这里后面也可以不跟参数值。

                                                                          • 1、用法上的不同
                                                                              从名字上可以看出来,PathVariable只能用于接收url路径上的参数,而RequestParam只能用于接收请求带的params,示例:

                                                                            	//使用@PathVariable接收参数,参数值需要在url进行占位, 前端传参
                                                                            	//的URL:url = ${ctx}/main/mm/am/edit/${Id}/${name}
                                                                            	@RequestMapping("/edit/{id}/{name}")publicStringedit(Model model,@PathVariablelong id,@PathVariableString name){returnpage("edit");}
                                                                            	//在url中输入:localhost:8080/**/?userName=zhangsan
                                                                            	//请求中包含username参数(如/requestparam1?userName=zhang),则自动传入。
                                                                            	publicStringqueryUserName(@RequestParamString userName)

                                                                              使用@PathVariable时,需要注意一个问题,如果想要url中占位符中的id值直接赋值到参数id中,需要保证url中的参数和方法接收参数一致,否则就无法接收。如果不一致的话,其实也可以解决,需要用@PathVariable中的value属性来指定对应关系。示例:

                                                                            	@RequestMapping("/user/{idd}")
                                                                            	publicStringtestPathVariable(@PathVariable(value ="idd")Integer id){
                                                                            		System.out.println("获取到的 id 为:"+ id);
                                                                            		return"success";
                                                                            	}
                                                                            • 2、内部参数不同
                                                                                PathVariable有value,name,required这三个参数;
                                                                                而RequestParam也有这三个参数,并且比PathVariable多一个参数defaultValue。

                                                                              • 3、应用的请求类型不同
                                                                                  PathVariable一般用于get和delete请求,RequestParam一般用于post请求。

                                                                                  不少应用为了实现RestFul的风格,采用@PathVariable这种方式。

                                                                                3.3 @RequestMapping

                                                                                  RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

                                                                                3.4.1 @RequestMapping注解的六个属性

                                                                                  RequestMapping注解有六个属性,下面分成三类进行说明:

                                                                                • 1、value, method(重要)
                                                                                    value: 指定请求的实际地址;
                                                                                    method: 指定请求的method类型, GET、POST、PUT、DELETE等,默认为GET;
                                                                                    代码示例:

                                                                                   	@RequestMapping(value ="/hello02", method =RequestMethod.POST)publicStringhello02(){return"success";}
                                                                                  • 2、consumes,produces
                                                                                      consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
                                                                                      produces: 指定返回值类型,不但可以设置返回值类型还可以设定返回值的字符编码;示例:

                                                                                    	@RequestMapping(value ="/test", produces="application/json;charset=UTF-8")
                                                                                    • 3、params,headers
                                                                                        params: 指定request中必须包含某些参数值时,才让该方法处理。
                                                                                        headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求。
                                                                                        代码示例:

                                                                                      	@RequestMapping( value ="/hello",params ={"username"})
                                                                                      	@RequestMapping( value ="/hello",headers ={"User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36"})
                                                                                      3.4.2 @RequestMapping注解使用的两个地方
                                                                                      • 1、类级别
                                                                                          映射请求的URL,示例:

                                                                                        	@Controller("accountController")
                                                                                        	@RequestMapping("/account")
                                                                                        	publicclassAccountController{
                                                                                        	
                                                                                        	}
                                                                                        • 2、方法级别
                                                                                            映射URL以及HTTP请求方法,示例:

                                                                                          	@RequestMapping(value="/removeAccount",params={"accountName","money>100"})
                                                                                          	publicStringremoveAccount(){
                                                                                          		System.out.println("删除了账户");
                                                                                          		return"success";
                                                                                          	}

                                                                                          3.4 @ResponseBody

                                                                                            注解 @ResponseBody,使用在控制层的方法上。

                                                                                            作用:@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据,需要注意的是,在使用此注解之后不会再走视图处理器,而是直接将数据写入到输入流中,他的效果等同于通过response对象输出指定格式的数据。

                                                                                            @ResponseBody:表示该方法的返回结果直接写入HTTPresponsebody中,一般在异步获取数据时使用,用于构建RESTFUL的API。在使用@RequestMapping后,返回值通常解析为跳转路径,加上@esponsebody后返回结果不会被解析为跳转路径,而是直接写入HTTP responsebody中。比如异步获取json数据,加@Responsebody后,会直接返回json数据。该注解一般会配合@RequestMapping一起使用。

                                                                                            如果返回对象,按utf-8编码。如果返回String,默认按iso8859-1编码,页面可能出现乱码。因此在注解中我们可以手动修改编码格式,例如@RequestMapping(value=“/cat/query”,produces=“text/html;charset=utf-8”),前面是请求的路径,后面是编码格式。

                                                                                            示例:

                                                                                            @RequestMapping("/login")
                                                                                            @ResponseBody
                                                                                            publicUserlogin(User user){
                                                                                              return user;
                                                                                            }

                                                                                            假设User字段:userName pwd,那么在前台接收到的数据为:

                                                                                          	'{"userName":"xxx","pwd":"xxx"}'

                                                                                            效果等同于如下代码:

                                                                                            @RequestMapping("/login")
                                                                                            publicvoidlogin(User user,HttpServletResponse response){
                                                                                              response.getWriter.write(JSONObject.fromObject(user).toString());
                                                                                            }

                                                                                            原理:控制层方法的返回值是如何转化为json格式的字符串的?其实是通过HttpMessageConverter中的方法实现的,它本是一个接口,在其实现类完成转换。如果是bean对象,会调用对象的getXXX()方法获取属性值并且以键值对的形式进行封装,进而转化为json串。如果是map集合,采用get(key)方式获取value值,然后进行封装。

                                                                                            @ResponseBody注解是将返回的数据结构转换为Json格式。所以 @RestController可以看作是@Controller和@ResponseBody的结合体,使用@RestController之后就不用再使用@Controller了。但是需要注意一个问题:如果是前后端分离,不用模板渲染的话,比如Thymeleaf,这种情况下是可以直接使用@RestController将数据以json格式传给前端,前端拿到之后解析;但如果不是前后端分离,需要使用模板来渲染的话,一般Controller中都会返回到具体的页面,那么此时就不能使用@RestController 了。示例:

                                                                                          	publicStringgetUser(){
                                                                                          		return"user";
                                                                                          	}

                                                                                            其实是需要返回到user.html页面的,如果使用@RestController的话,会将user作为字符串返回的,所以这时候我们需要使用@Controller注解。

                                                                                          3.5 @RequestBody

                                                                                            @RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);

                                                                                          GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。

                                                                                            示例:

                                                                                          @RequestMapping("/testRequestBody")publicStringtestRequestBody(@RequestBodyString body){System.out.println(body);return"success";}@RequestMapping("/testRequestJson")publicStringtestRequestJson(@RequestBodyUser user){System.out.println(user);return"success";}

                                                                                          3.6 @Component/@Controller/@Repository/@Service

                                                                                            在Spring2.5版本中,引入了更多的Spring类注解:

                                                                                          @Component、@Service、@Controller。

                                                                                            @Component是一个通用的Spring容器管理的单例bean组件注解。而@Repository, @Service, @Controller就是针对不同的使用场景所采取的特定功能化的注解组件。

                                                                                            总结:

                                                                                          • 1、@Component是通用注解,理论上可以在任意的类上进行添加,在扫描的时候都会完成Bean的注册其他三个注解是这个注解的拓展,并且具有了特定的功能 。

                                                                                            • 2、@Repository注解在持久层中,对数据库进行操作。

                                                                                              • 3、@Controller是控制层的注解,具有将请求进行转发,重定向的功能,包括调用Service层的方法。

                                                                                                • 4、@Service是业务逻辑层注解,这个注解只是标注该类处于业务逻辑层。

                                                                                                    用这些注解对应用进行分层之后,就能将请求处理,义务逻辑处理,数据库操作处理分离出来,为代码解耦,也方便了项目的维护和开发。

                                                                                                  3.7 @Cacheable/@CachePut/@CacheEvict

                                                                                                    常用的缓存注解有3个:@Cacheable、@CachePut和@CacheEvict。

                                                                                                  • 1、@Cacheable
                                                                                                      @Cacheable 的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
                                                                                                      @Cacheable 作用和配置方法:

                                                                                                    参数

                                                                                                    解释

                                                                                                    示例

                                                                                                    value

                                                                                                    缓存的名称,在 spring 配置文件中定义,必须指定至少一个

                                                                                                    @Cacheable(value=”mycache”)

                                                                                                    @Cacheable(value={”cache1”,”cache2”}

                                                                                                    key

                                                                                                    缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合

                                                                                                    @Cacheable(value=”testcache”,key=”#userName”)

                                                                                                    condition

                                                                                                    缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存

                                                                                                    @Cacheable(value=”testcache”,condition=”#userName.length()>2”)

                                                                                                      使用示例:@Cacheable(value=”accountCache”)。这个注释的意思是,当调用这个方法的时候,会从一个名叫accountCache的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的key就是参数userName,value就是Account对象。“accountCache”缓存是在 spring*.xml中定义的名称。较完整示例:

                                                                                                    	@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache 
                                                                                                    	publicAccountgetAccountByName(String userName){
                                                                                                    	     // 方法内部实现不考虑缓存逻辑,直接实现业务
                                                                                                    	     System.out.println("real query account."+userName); 
                                                                                                    	     returngetFromDB(userName); 
                                                                                                    	}
                                                                                                    • 2、@CachePut
                                                                                                        @CachePut的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和@Cacheable不同的是,它每次都会触发真实方法的调用。
                                                                                                        @CachePut作用和配置方法:

                                                                                                      参数

                                                                                                      解释

                                                                                                      例子

                                                                                                      value

                                                                                                      缓存的名称,在 spring 配置文件中定义,必须指定至少一个

                                                                                                      @CachePut(value=”my cache”)

                                                                                                      key

                                                                                                      缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合

                                                                                                      @CachePut(value=”testcache”,key=”#userName”)

                                                                                                      condition

                                                                                                      缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存

                                                                                                      @CachePut(value=”testcache”,condition=”#userName.length()>2”)

                                                                                                        @CachePut注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。示例代码:

                                                                                                      	@CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存
                                                                                                      	publicAccountupdateAccount(Account account){ 
                                                                                                      	   returnupdateDB(account); 
                                                                                                      	}
                                                                                                      • 3、@CacheEvict
                                                                                                          @CachEvict的作用主要针对方法配置,能够根据一定的条件对缓存进行清空。
                                                                                                          @CacheEvict作用和配置方法:

                                                                                                        参数

                                                                                                        解释

                                                                                                        例子

                                                                                                        value

                                                                                                        缓存的名称,在 spring 配置文件中定义,必须指定至少一个

                                                                                                        @CacheEvict(value=”my cache”)

                                                                                                        key

                                                                                                        缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合

                                                                                                        @CacheEvict(value=”testcache”,key=”#userName”)

                                                                                                        condition

                                                                                                        缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存

                                                                                                        @CacheEvict(value=”testcache”,condition=”#userName.length()>2”)

                                                                                                        allEntries

                                                                                                        是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存

                                                                                                        @CachEvict(value=”testcache”,allEntries=true)

                                                                                                        beforeInvocation

                                                                                                        是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存

                                                                                                        @CachEvict(value=”testcache”,beforeInvocation=true)

                                                                                                        显示详细信息

                                                                                                          示例:

                                                                                                        	@CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存  
                                                                                                        	publicvoidupdateAccount(Account account){
                                                                                                        	     updateDB(account); 
                                                                                                        	} 
                                                                                                        	
                                                                                                        	@CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存
                                                                                                        	publicvoidreload(){
                                                                                                        	     reloadAll()
                                                                                                        	}
                                                                                                        	
                                                                                                        	@Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache 
                                                                                                        	publicAccountgetAccountByName(String userName){ 
                                                                                                        	 // 方法内部实现不考虑缓存逻辑,直接实现业务
                                                                                                        	 returngetFromDB(userName); 
                                                                                                        	}

                                                                                                        3.8 Lombok(@Data注解)生成代码的“坑”

                                                                                                          Lombok的@Data注解会帮我们实现equals和hashcode方法,但是有继承关系时,Lombok自动生成的方法可能就不是我们期望的了。示例:定义一个Person类型,包含姓名和身份证两个字段:

                                                                                                        	@Data
                                                                                                        	classPerson{
                                                                                                        	    privateString name;
                                                                                                        	    privateString identity;
                                                                                                        	
                                                                                                        	    publicPerson(String name,String identity){
                                                                                                        	        this.name = name;
                                                                                                        	        this.identity = identity;
                                                                                                        	    }
                                                                                                        	}

                                                                                                          对于身份证相同、姓名不同的两个Person对象:

                                                                                                        	Person person1 =newPerson("zhuye","001");
                                                                                                        	Person person2 =newPerson("Joseph","001");
                                                                                                        	log.info("person1.equals(person2) ? {}", person1.equals(person2));

                                                                                                          使用equals判等会得到false。如果希望只要身份证一致就认为是同一个人的话,可以使用 @EqualsAndHashCode.Exclude 注解来修饰name字段,从equals和hashCode的实现中排除name字段:

                                                                                                          如果类型之间有继承,Lombok会怎么处理子类的equals和hashCode呢?

                                                                                                          @EqualsAndHashCode默认实现没有使用父类属性。为解决这个问题,我们可以手动设置callSuper开关为true,来覆盖这种默认行为:@EqualsAndHashCode(callSuper = true)。

                                                                                                        四、相关问题

                                                                                                        4.1 Spring MVC的控制器是不是单例模式

                                                                                                          控制器是单例模式,所以在多线程访问的时候有线程安全问题,不建议用同步,会影响性能。

                                                                                                          为什么设计成单例设计模式?

                                                                                                        性能(不用每次请求都创建对象)。

                                                                                                        不需要多例(不要在控制器中定义成员变量)。

                                                                                                          控制器最佳实践:

                                                                                                        不要在Controller中定义成员变量。

                                                                                                        万一必须要定义一个非静态成员变量时候,则通过注解@Scope("prototype"),将其设置为多例模式。

                                                                                                        4.2 Spring MVC怎样实现重定向和转发的

                                                                                                        • 1、转发
                                                                                                            在返回值前面加"forward:",譬如"forward:user.do?name=method4"。
                                                                                                             forward请求不经过视图处理器。
                                                                                                            转发的除了可以转发回到页面之外,还可以转发到其他请求中,示例:

                                                                                                          @RequestMapping("/forward")publicStringforward(){System.out.println("forward");return"forward:/index.jsp";}@RequestMapping("/forward2")publicStringforward2(){System.out.println("forward2");return"forward:/forward";}
                                                                                                          • 2、重定向
                                                                                                              在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com"。
                                                                                                              重定向操作也不会经过视图处理器。

                                                                                                            4.2.1 转发与重定向的区别
                                                                                                            • 1、转发
                                                                                                                由服务器的页面进行跳转,不需要客户端重新发送请求,特点:

                                                                                                              地址栏的请求不会发生变化,显示的还是第一次请求的地址;

                                                                                                              请求的次数,有且仅有一次请求;

                                                                                                              请求域中的数据不会丢失。

                                                                                                                转发示例:

                                                                                                              SpringBoot Spring MVC,第7张
                                                                                                              • 2、重定向
                                                                                                                  在浏览器端进行页面的跳转,需要发送两次请求(第一次是人为的,第二次是自动的),特点:

                                                                                                                地址栏的地址发生变化,显示最新发送请求的地址;

                                                                                                                请求次数:2次;

                                                                                                                请求域中的数据会丢失,因为是不同的请求。

                                                                                                                  重定向示例:

                                                                                                                SpringBoot Spring MVC,第8张
                                                                                                                • 3、转发和重定向的对比

                                                                                                                  区别

                                                                                                                  转发forward()

                                                                                                                  重定向sendRedirect()

                                                                                                                  根目录

                                                                                                                  包含项目访问地址

                                                                                                                  没有项目访问地址

                                                                                                                  地址栏

                                                                                                                  不会发生变化

                                                                                                                  会发生变化

                                                                                                                  哪里跳转

                                                                                                                  服务器端进行的跳转

                                                                                                                  浏览器端进行的跳转

                                                                                                                  请求域中数据

                                                                                                                  不会丢失

                                                                                                                  会丢失

                                                                                                                  4.2.2 转发和重定向的流程
                                                                                                                  • 1、重定向的流程
                                                                                                                      浏览器发送请求–>服务器运行–>相应请求–>,返回给浏览器一个新的地址与响应码,浏览器进行判断为重定向,自动发送一个新的请求给服务器,请求地址为刚刚服务器发送给浏览器的地址–>服务器运行–>相应请求。

                                                                                                                    • 2、转发的流程
                                                                                                                        发送请求 -->服务器运行->进行请求的重新设置,例如通过request.setAttribute(name,value)–>根据转发的地址,获取该地址的网页–>响应请求给浏览器。

                                                                                                                      4.3 如何解决GET/POST请求中文乱码问题

                                                                                                                      • 1、解决post请求乱码问题
                                                                                                                          post:必须要分别设置request和response的编码格式。
                                                                                                                          在web.xml中配置一个CharacterEncodingFilter过滤器(此过滤器为Spring MVC提供的),设置成utf-8,示例:

                                                                                                                        CharacterEncodingFilterorg.springframework.web.filter.CharacterEncodingFilterencodingutf-8CharacterEncodingFilter/*

                                                                                                                           如果在一个应用程序中可能会包含N多个(自定义)过滤器,这N多个过滤器一般是没有顺序的要求的。但是如果设置了编码过滤器,那么要求必须要将编码过滤器设置到最上面,保证编码过滤器优先执行。

                                                                                                                        • 2、解决get请求乱码问题
                                                                                                                           get请求中文参数出现乱码解决方法有两个:
                                                                                                                            1)改tomcat配置文件(server.xml)添加编码,与工程编码一致,该方式修改文件少,较常用,如下:

                                                                                                                          	

                                                                                                                             2)另外一种方法:对参数进行重新编码:

                                                                                                                          	String userName =newString(request.getParamter(“userName”)
                                                                                                                          		.getBytes(“ISO8859-1”),“utf-8”);

                                                                                                                            ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。

                                                                                                                          4.4 Spring MVC的异常处理

                                                                                                                            Spring MVC处理异常有3种方式:

                                                                                                                          • 1、使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver(不常用)
                                                                                                                              使用SimpleMappingExceptionResolver进行异常处理,具有集成简单、有良好的扩展性、对已有代码没有入侵性等优点,该方法仅能获取到异常信息,若在出现异常时,对需要获取除异常以外的数据的情况不适用。

                                                                                                                            • 2、实现Spring的异常处理接口HandlerExceptionResolver
                                                                                                                                使用实现HandlerExceptionResolver接口的异常处理器进行异常处理,具有集成简单、有良好的扩展性、对已有代码没有入侵性等优点,同时,在异常处理时能获取导致出现异常的对象,有利于提供更详细的异常处理信息。
                                                                                                                              示例:

                                                                                                                              publicclassCustomExceptionHandlerimplementsHandlerExceptionResolver{@OverridepublicModelAndViewresolveException(HttpServletRequest request,HttpServletResponse response,Object object,Exception exception){if(exception instanceofIOException){returnnewModelAndView("ioexp");}elseif(exception instanceofSQLException){returnnewModelAndView("sqlexp");}returnnull;}}

                                                                                                                                这个类必须声明到Spring配置文件中,或者使用@Component标签,让Spring管理它。

                                                                                                                              • 3、使用@ExceptionHandler注解实现异常处理
                                                                                                                                  使用@ExceptionHandler注解实现异常处理,具有集成简单、有扩展性好、不需要附加Spring配置等优点,在异常处理时不能获取除异常以外的数据。
                                                                                                                                  某个类中的异常处理示例:

                                                                                                                                	/*当使用ExceptionHandler进行处理的时候,默认会先走小范围,然后再寻找大范围
                                                                                                                                	 * 当在某一个类中定义的ExceptionHandler只能处理当前类的异常信息,如果其他类
                                                                                                                                	 * 包含的话,无法进行处理
                                                                                                                                	*/@ExceptionHandler(value ={ArithmeticException.class,NullPointerException.class})publicModelAndViewhandlerException(Exception exception){System.out.println("exception1");ModelAndView mv =newModelAndView();
                                                                                                                                        mv.setViewName("error");
                                                                                                                                        mv.addObject("exce",exception);return mv;}@ExceptionHandler(value ={Exception.class})publicModelAndViewhandlerException2(Exception exception){System.out.println("exception2");ModelAndView mv =newModelAndView();
                                                                                                                                        mv.setViewName("error");
                                                                                                                                        mv.addObject("exce",exception);return mv;}

                                                                                                                                  全局异常处理示例(在类上使用@ControllerAdvice注解):

                                                                                                                                @ControllerAdvicepublicclassMyGlobalExceptionHandler{@ExceptionHandler(value ={ArithmeticException.class,NullPointerException.class})publicModelAndViewhandlerException(Exception exception){System.out.println("global-------exception1");ModelAndView mv =newModelAndView();
                                                                                                                                        mv.setViewName("error");
                                                                                                                                        mv.addObject("exce",exception);return mv;}@ExceptionHandler(value ={Exception.class})publicModelAndViewhandlerException2(Exception exception){System.out.println("global-------exception2");ModelAndView mv =newModelAndView();
                                                                                                                                        mv.setViewName("error");
                                                                                                                                        mv.addObject("exce",exception);return mv;}}

                                                                                                                                  当局部注解@ExceptionHandler和全局注解@ControllerAdvice共存时,每次进行异常处理的时候,先在本类查找,然后去查找全局配置。

                                                                                                                                  Spring MVC集成异常处理3种方式都可以达到统一异常处理的目标。从3种方式的优缺点比较:

                                                                                                                                • 1、若只需要简单的集成异常处理,推荐使用SimpleMappingExceptionResolver即可;

                                                                                                                                  • 2、若需要集成的异常处理能够更具个性化,提供给用户更详细的异常信息,推荐自定义实现HandlerExceptionResolver接口的方式;

                                                                                                                                    • 3、若不喜欢Spring配置文件或要实现“零配置”,且能接受对原有代码的适当入侵,则建议使用@ExceptionHandler注解方式。

                                                                                                                                      4.5 怎么得到从前台传入的参数

                                                                                                                                        直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。示例:

                                                                                                                                      SpringBoot Spring MVC,第9张
                                                                                                                                      • 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象
                                                                                                                                          直接在方法中声明这个对象,Spring MVC就自动会把属性赋值到这个对象里面。

                                                                                                                                        4.6 Spring MVC中函数的返回值

                                                                                                                                          返回值可以有很多类型,有String、ModelAndView等。ModelAndView类把视图和数据都合并的一起的,但一般用String比较好。

                                                                                                                                        • 1、简单类型
                                                                                                                                            参数类型推荐使用包装数据类型,因为基础数据类型不可以为null:

                                                                                                                                          整型:Integer、int

                                                                                                                                          字符串:String

                                                                                                                                          单精度:Float、float

                                                                                                                                          双精度:Double、double

                                                                                                                                          布尔型:Boolean、boolean

                                                                                                                                          • 2、ModelAndView

                                                                                                                                            • 3、Json对象,要用到@ResponseBody(常用)

                                                                                                                                              • 4、void

                                                                                                                                                4.7 Spring MVC用什么对象从后台向前台传递数据

                                                                                                                                                • 1、使用Model对象
                                                                                                                                                    使用model对象往前台传递数据

                                                                                                                                                  SpringBoot Spring MVC,第10张

                                                                                                                                                    在jsp中接收从后台传递过来的参数

                                                                                                                                                  SpringBoot Spring MVC,第11张
                                                                                                                                                  • 2、使用HttpServletRequest对象
                                                                                                                                                      使用HttpServletRequest对象往前台传递数据

                                                                                                                                                    SpringBoot Spring MVC,第12张
                                                                                                                                                    • 3、使用Map对象(常用)
                                                                                                                                                        把Model改成Map即可。

                                                                                                                                                      4.8 怎么样把数据放入Session里

                                                                                                                                                        @SessionAttributes只能使用在类定义上,Spring MVC会将存放在model中对应的数据暂存到HttpSession中。

                                                                                                                                                      • 1、使用@SessionAttributes注解
                                                                                                                                                          可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 示例:

                                                                                                                                                        @Controller@SessionAttributes("linkList")publicclassRoomControl{
                                                                                                                                                        • 2、使用servlet-api(常用)

                                                                                                                                                          SpringBoot Spring MVC,第13张

                                                                                                                                                          4.9 Restful风格

                                                                                                                                                            REST,翻译过来叫做表现层状态转化,是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

                                                                                                                                                            我们在获取资源的时候就是进行增删改查的操作,如果是常规的架构风格,需要发送四个请求,分别是:

                                                                                                                                                          查询:localhost:8080/query?id=1

                                                                                                                                                          增加:localhost:8080/insert

                                                                                                                                                          删除:localhost:8080/delete?id=1

                                                                                                                                                          更新:localhost:8080/update?id=1

                                                                                                                                                            按照此方式发送请求的时候比较麻烦,需要定义多种请求,所以产生了能让不同的请求方式表示不同的请求类型,即Restful风格:

                                                                                                                                                          GET:获取资源 /book/1

                                                                                                                                                          POST:新建资源 /book

                                                                                                                                                          PUT:更新资源 /book/1

                                                                                                                                                          DELETE:删除资源 /book/1

                                                                                                                                                            示例:

                                                                                                                                                          packagecom.test.controller;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.servlet.view.InternalResourceViewResolver;@ControllerpublicclassRestController{@RequestMapping(value ="/user",method =RequestMethod.POST)publicStringadd(){System.out.println("添加");return"success";}@RequestMapping(value ="/user/{id}",method =RequestMethod.DELETE)publicStringdelete(@PathVariable("id")Integer id){System.out.println("删除:"+id);return"success";}@RequestMapping(value ="/user/{id}",method =RequestMethod.PUT)publicStringupdate(@PathVariable("id")Integer id){System.out.println("更新:"+id);return"success";}@RequestMapping(value ="/user/{id}",method =RequestMethod.GET)publicStringquery(@PathVariable("id")Integer id){System.out.println("查询:"+id);return"success";}}

                                                                                                                                                            此时浏览器form表单只支持GET与POST请求,而DELETE、PUT等method并不支持。对于此问题,Spring3.0添加了一个HiddenHttpMethodFilter过滤器,可以将这些请求转换为标准的http方法,使得支持GET、POST、PUT与DELETE请求。配置示例:

                                                                                                                                                          hiddenorg.springframework.web.filter.HiddenHttpMethodFilterhidden/*

                                                                                                                                                          4.10 Spring MVC里面拦截器是怎么写的

                                                                                                                                                          4.10.1 单个拦截器的使用

                                                                                                                                                            SpringMVC提供了拦截器机制,允许运行目标方法之前进行一些拦截工作或者目标方法运行之后进行一下其他相关的处理。自定义的拦截器必须实现HandlerInterceptor接口。

                                                                                                                                                            拦截器方法的执行时机:

                                                                                                                                                          • 1、preHandle
                                                                                                                                                              这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求request进行处理。如果程序员决定该拦截器对请求进行拦截处理后还要调用其他的拦截器,或者是业务处理器去进行处理,则返回true;如果程序员决定不需要再调用其他的组件去处理请求,则返回false。

                                                                                                                                                            • 2、postHandle
                                                                                                                                                                这个方法在业务处理器处理完请求后,但是DispatcherServlet 向客户端返回响应前被调用,在该方法中对用户请求request进行处理。

                                                                                                                                                              • 3、afterCompletion
                                                                                                                                                                  这个方法在DispatcherServlet完全处理完请求后被调用,可以在该方法中进行一些资源清理的操作。

                                                                                                                                                                  有两种写法,一种是实现org.springframework.web.servlet.HandlerInterceptor接口;另外一种是继承适配器org.springframework.web.servlet.handler.HandlerInterceptorAdapter类,接着在接口方法当中,实现处理逻辑;最后在Spring MVC的配置文件中配置拦截器即可:

                                                                                                                                                                  先看实现接口的方式:

                                                                                                                                                                publicclassMyInterceptorimplementsHandlerInterceptor{/**
                                                                                                                                                                     * 在处理器处理具体的方法之前开始执行
                                                                                                                                                                     * @return      注意返回值,如果返回值是false表示请求处理到此为止,如果是true,才会接着向下执行
                                                                                                                                                                     */publicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{System.out.println(this.getClass().getName()+"----preHandle");returntrue;}/**
                                                                                                                                                                     * 在处理器完成方法的处理之后执行
                                                                                                                                                                     */publicvoidpostHandle(HttpServletRequest request,HttpServletResponse response,Object handler,ModelAndView modelAndView)throwsException{System.out.println(this.getClass().getName()+"----postHandle");}/**
                                                                                                                                                                     * 整个servlet处理完成之后才会执行,主要做资源清理的工作
                                                                                                                                                                     */publicvoidafterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex)throwsException{System.out.println(this.getClass().getName()+"----afterCompletion");}}

                                                                                                                                                                  拦截器的执行顺序如下:

                                                                                                                                                                拦截器的preHandle方法----》执行目标方法----》执行拦截器的postHandle方法----》执行页面跳转----》执行拦截器的afterCompletion方法

                                                                                                                                                                  在配置拦截器的时候有两个需要注意的点:

                                                                                                                                                                1. 如果prehandle方法返回值 为false,那么意味着不放行,那么就会造成后续的所有操作都中断。

                                                                                                                                                                1. 如果执行到方法中出现异常,那么后续流程不会处理但是afterCompletion方法会执行。

                                                                                                                                                                4.10.2 多个拦截器的使用

                                                                                                                                                                  定义了多个拦截器时,谁先执行取决于配置的顺序。

                                                                                                                                                                1. 拦截器的preHandle是按照顺序执行的。

                                                                                                                                                                1. 拦截器的postHandle是按照逆序执行的。

                                                                                                                                                                1. 拦截器的afterCompletion是按照逆序执行的。

                                                                                                                                                                1. 如果执行的时候核心的业务代码出问题了,那么已经通过的拦截器的afterCompletion会接着执行。

                                                                                                                                                                4.10.3 过滤器和拦截器的区别
                                                                                                                                                                1. 过滤器是基于函数回调的,而拦截器是基于Java反射的。

                                                                                                                                                                1. 过滤器依赖于servlet容器,而拦截器不依赖与Servlet容器。

                                                                                                                                                                1. 过滤器几乎对所有的请求都起作用,而拦截器只能对action请求起作用。

                                                                                                                                                                1. 拦截器可以访问action的上下文,而过滤器不可以。

                                                                                                                                                                1. 在controller的生命周期中,拦截器可以多次调用,而过滤器只能在web容器初始化的时候初始化一次,后续匹配的所有请求都会经过过滤器来进行过滤。

                                                                                                                                                                  拦截器跟过滤器的执行流程:

                                                                                                                                                                SpringBoot Spring MVC,第14张

                                                                                                                                                                  拦截器和过滤器的包含关系:

                                                                                                                                                                SpringBoot Spring MVC,第15张
                                                                                                                                                                4.10.4 静 态 资 源 被 拦 截 问

                                                                                                                                                                  Spring Boot 2.0废弃了WebMvcConfigurerAdapter,但是WebMvcConfigurationSupport又会导致默认的静态资源被拦截,这就需要我们手动将静态资源放开。此时有两种方法解决。

                                                                                                                                                                • 1、重写addResourceHandlers方法
                                                                                                                                                                    在继承WebMvcConfigurationSupport的配置类中重写addResourceHandlers,示例:

                                                                                                                                                                  	/**
                                                                                                                                                                  	* 用来指定静态资源不被拦截,否则继承 WebMvcConfigurationSupport 这种方式
                                                                                                                                                                  	会导致静态资源无法直接访问
                                                                                                                                                                  	* @param registry
                                                                                                                                                                  	*/@Override
                                                                                                                                                                  	protectedvoidaddResourceHandlers(ResourceHandlerRegistry registry){
                                                                                                                                                                  		registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
                                                                                                                                                                  		super.addResourceHandlers(registry);
                                                                                                                                                                  	}
                                                                                                                                                                  • 2、实现 WebMvcConfigurer 接口
                                                                                                                                                                      重写addInterceptors方法,将自定义的拦截器添加进去,示例:

                                                                                                                                                                    	@Configuration
                                                                                                                                                                    	publicclassMyInterceptorConfigimplementsWebMvcConfigurer{
                                                                                                                                                                    		@Override
                                                                                                                                                                    		publicvoidaddInterceptors(InterceptorRegistry registry){
                                                                                                                                                                    			// 实现 WebMvcConfigurer 不会导致静态资源被拦截
                                                                                                                                                                    			registry.addInterceptor(newMyInterceptor()).addPathPatterns("/**");
                                                                                                                                                                    		}
                                                                                                                                                                    	}

                                                                                                                                                                    4.11 加锁要考虑锁的粒度和场景问题

                                                                                                                                                                      通常情况下业务代码是分层的,数据经过无状态的Controller、Service、Repository流转到数据库,没必要使用synchronized。

                                                                                                                                                                      滥用synchronized可能会极大地降低性能。使用Spring框架时,默认情况下 Controller、Service、Repository是单例的,加上synchronized会导致整个程序几乎就只能支持单线程,造成极大的性能问题。

                                                                                                                                                                    4.12 捕获和处理异常容易犯的错

                                                                                                                                                                    • 1、不在业务代码层面考虑异常处理,仅在框架层面粗犷捕获和处理异常
                                                                                                                                                                        不建议在框架层面进行异常的自动、统一处理,尤其不要随意捕获异常。但,框架可以做兜底工作。如果异常上升到最上层逻辑还是无法处理的话,可以以统一的方式进行异常转换,比如通过 @ControllerAdvice + @ExceptionHandler,来捕获这些“未处理”异常:

                                                                                                                                                                      对于自定义的业务异常,以Warn级别的日志记录异常以及当前URL、执行方法等信息后,提取异常中的错误码和消息等信息,转换为合适的API包装体返回给API调用方;

                                                                                                                                                                      对于无法处理的系统异常,以Error级别的日志记录异常和上下文信息(比如URL、参数、用户ID)后,转换为普适的“服务器忙,请稍后再试”异常信息,同样以API包装体返回给调用方。

                                                                                                                                                                        示例:

                                                                                                                                                                      	@RestControllerAdvice
                                                                                                                                                                      	@Slf4j
                                                                                                                                                                      	publicclassRestControllerExceptionHandler{
                                                                                                                                                                      	    privatestaticint GENERIC_SERVER_ERROR_CODE =2000;
                                                                                                                                                                      	    privatestaticString GENERIC_SERVER_ERROR_MESSAGE ="服务器忙,请稍后再试";
                                                                                                                                                                      	
                                                                                                                                                                      	    @ExceptionHandler
                                                                                                                                                                      	    publicAPIResponsehandle(HttpServletRequest req,HandlerMethod method,Exception ex){
                                                                                                                                                                      	        if(ex instanceofBusinessException){
                                                                                                                                                                      	            BusinessException exception =(BusinessException) ex;
                                                                                                                                                                      	            log.warn(String.format("访问 %s -> %s 出现业务异常!", req.getRequestURI(), method.toString()), ex);
                                                                                                                                                                      	            returnnewAPIResponse(false,null, exception.getCode(), exception.getMessage());
                                                                                                                                                                      	        }else{
                                                                                                                                                                      	            log.error(String.format("访问 %s -> %s 出现系统异常!", req.getRequestURI(), method.toString()), ex);
                                                                                                                                                                      	            returnnewAPIResponse(false,null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
                                                                                                                                                                      	        }
                                                                                                                                                                      	    }
                                                                                                                                                                      	}
                                                                                                                                                                      • 2、捕获了异常后直接生吞
                                                                                                                                                                          在任何时候,我们捕获了异常都不应该生吞,也就是直接丢弃异常不记录、不抛出。
                                                                                                                                                                          这样的处理方式还不如不捕获异常,因为被生吞掉的异常一旦导致Bug,就很难在程序中找到蛛丝马迹,使得Bug排查工作难上加难。

                                                                                                                                                                        • 3、丢弃异常的原始信息
                                                                                                                                                                            像这样调用readFile方法,捕获异常后,完全不记录原始异常,直接抛出一个转换后异常,导致出了问题不知道IOException具体是哪里引起的:

                                                                                                                                                                          	@GetMapping("wrong1")
                                                                                                                                                                          	publicvoidwrong1(){
                                                                                                                                                                          	    try{
                                                                                                                                                                          	        readFile();
                                                                                                                                                                          	    }catch(IOException e){
                                                                                                                                                                          	        //原始异常信息丢失  
                                                                                                                                                                          	        thrownewRuntimeException("系统忙请稍后再试");
                                                                                                                                                                          	    }
                                                                                                                                                                          	}

                                                                                                                                                                            或者是这样,只记录了异常消息,却丢失了异常的类型、栈等重要信息:

                                                                                                                                                                          	catch(IOException e){
                                                                                                                                                                          	    //只保留了异常消息,栈没有记录
                                                                                                                                                                          	    log.error("文件读取错误, {}", e.getMessage());
                                                                                                                                                                          	    thrownewRuntimeException("系统忙请稍后再试");
                                                                                                                                                                          	}

                                                                                                                                                                            这两种处理方式都不太合理,可以改为如下方式:

                                                                                                                                                                          	catch(IOException e){
                                                                                                                                                                          	    log.error("文件读取错误", e);
                                                                                                                                                                          	    thrownewRuntimeException("系统忙请稍后再试");
                                                                                                                                                                          	}

                                                                                                                                                                            或者,把原始异常作为转换后新异常的cause,原始异常信息同样不会丢:

                                                                                                                                                                          	catch(IOException e){
                                                                                                                                                                          	    thrownewRuntimeException("系统忙请稍后再试", e);
                                                                                                                                                                          	}
                                                                                                                                                                          • 4、抛出异常时不指定任何消息
                                                                                                                                                                              示例:

                                                                                                                                                                            	thrownewRuntimeException();

                                                                                                                                                                              如果捕获了异常打算处理的话,除了通过日志正确记录异常原始信息外,通常还有三种处理模式:

                                                                                                                                                                            转换,即转换新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个无关或没有任何信息的异常,并最好通过cause关联老异常。

                                                                                                                                                                            重试,即重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试。

                                                                                                                                                                            恢复,即尝试进行降级处理,或使用默认值来替代原始数据。

                                                                                                                                                                            4.13 跨域问题

                                                                                                                                                                              使用SpringMVC时,全局性质的跨域配置示例:

                                                                                                                                                                            @Configuration@EnableWebMvcpublicclassWebConfigextendsWebMvcConfigurerAdapter{@OverridepublicvoidaddCorsMappings(CorsRegistry registry){
                                                                                                                                                                                    registry.addMapping("/**");}}

                                                                                                                                                                              使用SpringBoot时,可以改成:

                                                                                                                                                                            @ConfigurationpublicclassMyConfiguration{@BeanpublicWebMvcConfigurercorsConfigurer(){returnnewWebMvcConfigurerAdapter(){@OverridepublicvoidaddCorsMappings(CorsRegistry registry){
                                                                                                                                                                                            registry.addMapping("/**");}};}}

                                                                                                                                                                              作为上述其他方法的替代,Spring框架还提供了CorsFilter,示例:

                                                                                                                                                                            @ConfigurationpublicclassMyConfiguration{@BeanpublicFilterRegistrationBeancorsFilter(){UrlBasedCorsConfigurationSource source =newUrlBasedCorsConfigurationSource();CorsConfiguration config =newCorsConfiguration();
                                                                                                                                                                                    config.setAllowCredentials(true);
                                                                                                                                                                                    config.addAllowedOrigin("http://domain1.com");
                                                                                                                                                                                    config.addAllowedHeader("*");
                                                                                                                                                                                    config.addAllowedMethod("*");
                                                                                                                                                                                    source.registerCorsConfiguration("/**", config);FilterRegistrationBean bean =newFilterRegistrationBean(newCorsFilter(source));
                                                                                                                                                                                    bean.setOrder(0);return bean;}}

                                                                                                                                                                              @CrossOrigin不起作用的原因:

                                                                                                                                                                            • 1、SpringMVC的版本要在4.2或以上版本才支持@CrossOrigin。

                                                                                                                                                                              • 2、不正确的请求导致无法得到预期的响应,导致浏览器端提示跨域问题。

                                                                                                                                                                                • 3、在Controller注解上方添加@CrossOrigin注解后,仍然出现跨域问题,解决方案之一就是:在@RequestMapping注解中没有指定Get、Post方式,示例:

                                                                                                                                                                                  @CrossOrigin@RestControllerpublicclass person{@RequestMapping(method =RequestMethod.GET)publicStringadd(){// 若干代码}}

                                                                                                                                                                                  4.14 Spring中的事件类型

                                                                                                                                                                                    Spring的事件通知机制是一项很有用的功能,使用事件机制可以将相互耦合的代码解耦,从而方便功能的修改与添加。

                                                                                                                                                                                    举个例子,假设有一个添加评论的方法,在评论添加成功之后需要进行修改Redis缓存、给用户添加积分等等操作。在以前的代码中,可以使用观察者模式来解决这个问题。Spring中已经存在了一个升级版观察者模式的机制,这就是监听者模式。通过该机制,我们就可以发送接收任意的事件并处理。

                                                                                                                                                                                    监听者模式包含了一个监听者Listener与之对应的事件Event,还有一个事件发布者EventPublish,过程就是EventPublish发布一个事件,被监听者捕获到,然后执行事件相应的方法。
                                                                                                                                                                                  4.14.1 五种事件类型

                                                                                                                                                                                    Spring 提供了以下5种标准的事件:

                                                                                                                                                                                  • 1、上下文更新事件
                                                                                                                                                                                     该事件会在ApplicationContext被初始化或者更新时发布。也可以在调用ConfigurableApplicationContext接口中的refresh()方法时被触发。

                                                                                                                                                                                    • 2、上下文开始事件
                                                                                                                                                                                       当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。

                                                                                                                                                                                      • 3、上下文停止事件
                                                                                                                                                                                         当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。

                                                                                                                                                                                        • 4、上下文关闭事件
                                                                                                                                                                                           当ApplicationContext被关闭(close()方法)时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。

                                                                                                                                                                                          • 5、请求处理事件
                                                                                                                                                                                             在Web应用中,当一个http请求(request)结束触发该事件。

                                                                                                                                                                                            4.14.2 自定义事件

                                                                                                                                                                                              如果一个Bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,Bean会自动被通知,其实就是接下来要说的自定义事件。

                                                                                                                                                                                              除了上面介绍的五种事件以外,还可以通过扩展ApplicationEvent 类来开发自定义的事件。示例:

                                                                                                                                                                                            	//自定义事件对象
                                                                                                                                                                                            	publicclassCustomApplicationEventextendsApplicationEvent{
                                                                                                                                                                                            	    publicCustomApplicationEvent(Object source,finalString msg ){
                                                                                                                                                                                            	        super(source);
                                                                                                                                                                                            	        System.out.println("Created a Custom event");
                                                                                                                                                                                            	    }
                                                                                                                                                                                            	}
                                                                                                                                                                                            	//事件对应的监听器
                                                                                                                                                                                            	publicclassCustomEventListenerimplementsApplicationListener{
                                                                                                                                                                                            	    @Override
                                                                                                                                                                                            	    publicvoidonApplicationEvent(CustomApplicationEvent applicationEvent){
                                                                                                                                                                                            	        //handle event
                                                                                                                                                                                            	    }
                                                                                                                                                                                            	}
                                                                                                                                                                                            	//通过applicationContext接口的publishEvent()方法来发布自定义事件
                                                                                                                                                                                            	CustomApplicationEvent customEvent = 
                                                                                                                                                                                            		newCustomApplicationEvent(applicationContext,"Test message");
                                                                                                                                                                                            	applicationContext.publishEvent(customEvent);