目录
前言1:首先要明白,什么是统一功能?
前言2:统一功能包括哪些呢?展开说说?
一、拦截器(interceptor)
1、介绍
2、如何使用拦截器
3、拦截器的在程序内部的执行流程是啥呢?
4、拦截器在代码底层是如何被成功执行的?(源码分析)
二、统一异常处理
1、大概介绍
2、具体操作
三、统一数据返回处理
1、大概介绍
2、具体讲解
3、出现的问题
4、解决办法
5、为什么会报错
四、补充知识(适配器模式)
1、大概介绍
2、代码讲解
五、在源码层面进行分析,统一异常处理和统一数据格式处理是咋执行的
1、大概讲解
2、插播几条知识
3、具体讲解
前言1:首先要明白,什么是统一功能?
例如:六国统一,秦始皇规定,我的国家货币要统一,只能使用我规定的那一种货币,那为什么要这样做呢?方便管理,各国之间的货币之间也不方便流通。
统一功能:像六国统一货币那样,规定某些程序,统一执行某些操作,也方便进行管理,提高效率。
前言2:统一功能包括哪些呢?展开说说?
- 使用拦截器实现某些操作,主要拦截用户在页面发来的请求,例如使用拦截器实现用户登录权限的统一校验。
- 某一块程序产生运行时的异常后,统一对这些异常进行处理。
- 某些数据要返回给前端时,对数据的格式进行统一处理。
一、拦截器(interceptor)
1、介绍
例:一个学校呢,可能进入车,也可能进入人,安排出一个保安,放到门口,保安要对这些进行拦截校验,现在领导要求保安,拦截人,对人进行校验,不管车。
拦截器就相当于是一个保安,拦截用户请求,按照规定,要对某些方法(人)拦截,先去执行预先设定好的代码(进行校验)。
2、如何使用拦截器
咱们先了解一下大概步骤,整体看一下~~:
- 第一步:先自定义出一个拦截器(相当于安排出一个保安,告诉他的职责是什么,(拦截,校验))
- 第二步:再将拦截器注册到项目中(将保安安排到某个地方,去发挥作用,再告诉具体拦截谁)
大概了解完之后,那咱们看具体是如何使用的吧(以登录验证为例)~:
- 第一步:先自定义出一个拦截器吧~,名字:LoginInterceptor,具有拦截,校验的功能,可以实现(implements)HandlerInterceptor(代理拦截器),并重写出此接口内的所有方法,下面是对这三个方法的解释:
preHandle(相当于保安将人拦截后,在对人进行校验):在目标方法执行前执行,校验通过,返回true,失败就返回false~
postHanddle:在目标方法执行后执行~
afterCompletion:这个方法现在不用学了,因为这个方法是在视图渲染后执行的,而现在后端已经不涉及视图了~
- 第二步:再将拦截器注册配置到项目中:创建出WebConfig(网络配置)类,再去实现WebMvcConfiguer接口~,接着将对象注入到这个类中,在类中重写接口中的addInterceptor方法,调用registry的addInterceptor方法,这个方法是添加注入到类中的拦截器,再规定这个拦截器可以具体拦截哪些路径,具体不拦截哪些路径。(相当于告诉保安要具体拦截哪些车牌号的车,哪些车牌号不用拦截)具体操作如下图所示~
此时注意看18-21行代码。只用几行代码,就可以对所有想要验证的方法统一处理~
相信在此时,大家更加直接看出拦截器的强大了吧,按照18-21行的做法,大大提高了开发的效率,简化了代码~
---------------------------------------------------------------------------------------------------------------------------------
但是在此时,有一个问题,由于咱们的项目现在没有前后端分离,所以咱们刚才配置的拦截器不仅会拦截后端的请求,还会拦截前端的请求,甚至是前端的图片也会拦截,但是咱们刚才设置的是拦截所有请求,只允许它不拦截后端的登录请求,并没有对前端的登录请求放行,比如在浏览器输入http://127.0.0.1:8080/login.html,会进行拦截,那可咋办呢?
(下面的方法借助于他人博客,链接为:Spring统一功能-CSDN博客)
通过excludePathPatterns()加入需要放行的前端路径
方法一:把前端需要放行的文件都放到一个文件夹,例如book中,然后/book/**就可以
方法二:不放到文件夹中,一个文件一个文件的放行。(但是不能排除其他文件中会不会有html文件呀,这样就仿佛小偷混进了好人的包里面,逃出去了)。。。
(但是上面两种方法不能排除其他文件中会不会有html文件呀,这样就仿佛小偷混进了好人的包里面,逃出去了)。。。下面请看第三种方法~
方法三:不放到文件夹中,精准放行,不会让任何一个漏网之鱼逃出去。
放行方法的另一种方式:(将那些路径写到集合当中,代码比较优雅~)
-----------------------------------------------------------------------------------------------------------------------------
这是一些拦截路径的格式,大家可以看一看。
3、拦截器的在程序内部的执行流程是啥呢?
相信大家对上述所讲大概已经明白了拦截器的基本操作,但是可能有人还会有新的疑惑,那它在内部是咋执行的呢?现在告诉大家~
在没有拦截器之前,用户的任何一个请求都是如下图所示~
将拦截器注册配置到项目中之后,请求流程都是如下图所示~
4、拦截器在代码底层是如何被成功执行的?(源码分析)
好,现在对于拦截器已经基本上介绍完毕了,下面是要具体看看它的底层到底是如何实现的~
(了解即可,因为源码不容易看懂)
现在也是按照上面讲的逻辑,咱们先讲出大概的框架,让各位看官心里有个大概了解,然后咱们再讲具体的~ 说到这里,我联想到,看源码,也是这样一个逻辑~,因为源码一般看不懂,因为不仅多而且逻辑复杂,所以在看源码的过程,我们要有以下两个原则:
一:不死扣源码的每一行,找到想要看的源码那一部分,大概浏览一下,找到核心的代码,连蒙带猜的去看,遇到不懂的方法,按住CTRL,点击左键,去看具体它是如何实现的。
二:若经过第一步后,还是看不懂,就不要挣扎了,在不同的业务下,多遇到几次,多看几次,多研究几次,就可以了。
好,咱们言归正传
大概流程:核心类是DispatcherServlet,它属于一个调度器,(相当于一个公司前台一样),为spring服务,听从spring差遣,当用户发送请求后,Spring通过它来告诉我们的程序该去调用什么,来完成这个请求,在执行请求之前,会去执行拦截器的相关代码,然后看拦截器检验是否成功,再去决定要不要继续向下执行。那为什么DispatcherServlet是核心类呢?请看下图所示:
spring产生之后,DispatcherServlet接着被产生,然后就去执行拦截器相关的代码了,所以DispatcherServlet是一个核心类。(当程序启动后,结束之前,初始化只会初始一次)。
ok,咱们大概的流程看完了,看每一步具体是怎么执行的,是怎么就执行到拦截器的代码了~
首先,既然DispatcherServlet类是一个核心类,那咱们从DispatcherServlet类入手,咱们去研究DispatcherServlet类,看看它到底为spring做了什么事,他其实就是一个servlet,servlet的生命周期是:
init()-初始化,service()-处理请求,derstory()-服务终止。
先去初始化过程:
咱们要先找到DispatcherServlet类,所以先按住 CTRL+n 快捷键,在输入框输入DispatcherServlet找到这个类,点击进入这个类:
进入它的父类(CTRL + 鼠标左键):
再进入FrameworkServlet的父类:
再找到HttpServletBean中的init:
再进入init方法中的initServletBean()方法:
进去之后,发现没有具体实现逻辑,点击箭头处
此刻又回到了DispatcherServlet的父类FrameworkServlet中:
观看大概逻辑后,发现try里面的才是核心逻辑,不知道绿色的里面是啥东西,咱们点进去一探究竟
点进去之后,发现此方法的返回类型是WebApplicationContext,方法里面有个wac变量,咱们看看~
可以看到上面的if语句,咱们猜一猜,先是获取一些东西,再给wac,给了之后呢,传给onRefresh方法,这个方法猜着是更新的意思,那具体啥意思呢,咱们点进去看看~
点进去之后,发现是空的,咱们点击箭头处看看~
进去之后看到,wac传给了context,这个似曾相识呀!这个不就是spring的上下文吗?也就是spring的运行环境,也是一个容器,这个容器里面可以存放对象,那现在用context干什么呢?好像是在里面放东西呀 ,放的就是Spring的九大组件,也就是在初始化这个容器, 那是谁在初始化,是spring,它是根据什么初始化呢?根据程序中的各种注解和代码和配置等等各种方面完成的这个context~
service()-处理请求的过程:
开始处理请求了,咱们看看具体怎么处理请求的~ 进入doService方法:
然后咱们不一行一行看了,由于DispatcherServlet是一个调度器,所以咱们直接看doService方法里面的doDispatch方法。先大概介绍一下这个方法,是个调度方法,管着请求发出之后,谁干什么事的,在这里面呢,咱们一会重要讨论的是两个器,一个处理器,一个调度器,讨论完之后,会继续向下看代码,也就是关于过滤器相关的代码,对!没错,终于执行到过滤器了,会讲解在这个方法里,怎么执行的过滤器相关的代码~ 废话不多说,开整!
点进去之后,直接找核心的代码,mappedHander与HandlerAdapter。
这两个对于咱们来说,还有一点陌生,因为还没学呢~简单介绍一下吧~
mappedHander:它是一个处理器,作用是啥呢?比方说我们打一个1235电话后,会提示我们根据我们的需求,按不同的键,接线员就会给我分配不同的部门处理,它就好比是一个接线员,当一个请求发进来之后,Spring会通过调度器让处理器去处理请求,它会根据不同路径去调用不同的controller方法,完成响应。
HandlerAdapter:看上面的图片,根据一个处理器,得到了一个适配器ha,它就好比是一个中间人,当接线员了解到打电话的人的需求后,接线员会亲自把需求给相关的部门吗,不会的,他会把需求转接给一个中间人,再由这个中间人去对接部门,所以是适配器去调用不同的controller方法,完成响应。
刚看,确实很复杂,多看看就好啦~ 咱们接着往下看~
大家看applyPreHandle方法,是不是又是似曾相识呢?这不就是拦截器相关的方法吗?真的是这样吗?咱们点进去看看就知道了~
好家伙,真的是这样的~ 终于看到拦截器相关的代码了!在这个方法里面得到咱们注册配置的拦截器,再去执行它的preHandle方法。
若preHandle返回true,代表对目标方法的检验成功,检验成功之后,就去调用适配器了,去调用各种controller方法,完成对用户的响应,那这个过程是怎实现的呢?咱们继续往下看~
最后上图中这个applyPreHandle方法最后在154行处,会返回true。
接着true返回到下图中的1067行代码处之后,if语句判断为false,继续向下执行,执行下图的1072行
检验成功之后,就去调用ha适配器了,去调用各种controller方法 ,完成响应。
若preHandle返回false:,代表对目标方法的检验失败,程序不再向下执行,那具体咋执行的呢?咱们接着看~
在下图中的第150行代码处返回false,doDispatch方法执行完成
doDispatch方法执行完成后,再执行finall里面的方法后,doService的方法也执行完成了。
derstory()-服务终止: 这个暂时不作讲解了,继续下面的二,三的讲解了,要不然讲不完了,下面的比较重要~
其实这么一看这三个过程,我认为初始化阶段就好像是在商店准备开门的准备阶段,service阶段就好像是准备好了,等待服务的过程~
综上所述,拦截器终于完结了,不知道大家掌握的怎么样呢?~
二、统一异常处理
1、大概介绍
咱们先大概讲一下,这个异常~ 当程序在运行时,产生了异常,
但是对于我们后端来说:不想要将具体的错误,返回给前端~
并且对于前端来说:也不想要将具体错误,给用户,只给用户展现一个简单页面即可。
这种现象与一个公司出现问题时,所做的差不多,因为公司会对外界说,公司内部错误,不变告知。
所以我们也将异常的结果进行统一处理,统一返回一个结果。
好的,现在大概讲完了这个统一异常处理,现在咱们将统一异常处理是怎么实现的~
2、具体操作
咱们是创建出一个异常处理类,在类中写出对异常进行捕获的代码,如下图所示,写出了三个异常处理器,对程序产生的异常进行捕获。
那现在问题来了!异常是怎么被这几个处理器捕获的呢?
当异常产生之后,会自动进行捕获,这三个处理器会进行比较,看谁距离异常近,就由谁进行捕获,这与处理器代码的先后没有任何关系~
看谁距离异常近,这句话是啥意思呢?看咱们的三个异常处理器的参数,是三个不同的异常类型,
当异常出现之后,会看谁的类型与出现的异常的类型最相近,就由谁来捕获~
三、统一数据返回处理
1、大概介绍
咱们还是先大概介绍一下吧~
统一数据返回处理就是将程序中所有接口返回的数据进行统一处理,包装一下,统一返回一种类型,这样对前端来说,每次返回都是一种类型,也方便他们接收了,对于后端来说呢,进行统一处理,而不用一个接口一个接口的处理,大大提高了效率~
2、具体讲解
在代码层面具体如何进行统一处理的呢?实现ResponBodyAdvice接口,添加@ControllerAdvice注解,重写两个方法,第一个方法返回true,第二个方法返回数据~
3、出现的问题
在数据进行包装之前,类型已经是要求的统一类型了,又包装了一层~
返回String类型的数据时,报错,显示类型不匹配~
4、解决办法
在包装之前,先判断一下body的类型:
如果是result类型的,就不用再包装了,直接return body,
如果是String类型的,对包装之后的数据进行处理。
5、为什么会报错
只简单说一下~
是因为在包装完之后,还会进行别的操作,只不过咱们看不见~
当包装完的String类型数据,变成我们统一的类型后,又把这个数据,传给了一个方法,用String类型的参数接收,由于这俩类型没啥关系,不能直接转换,所以就报了类型转换错误~
当然了,只有String类型的数据如此特殊,其余类型的数据不会报错~
四、补充知识(适配器模式)
1、大概介绍
也是大概介绍一下,先有个大概了解~
比方说。中国和美国的插座,咱们如果去了美国,咱们国内的电器不能直接使用美国的插座,所以就需要一个转换插座,把转换插座插到美国插座上,然后咱们国内电器使用这个转换插座,不直接使用美国的插座~
对于咱们的Java程序来说,美国插座相当于一个类,电器相当于另一个类,转换插座就相当于一个适配器,现在有一个业务,需要两个类结合使用,完成某一项任务,但是这两个类不兼容,无法结合,所以适配器隆重登场,适配器可以兼容这两个类,这样,这两个类通过适配器结合在一起了,只不过是间接结合而已,在适配器中去完成这一项任务~
2、代码讲解
大概讲解完了,咱们通过代码去实战演练一下~
五、在源码层面进行分析,统一异常处理和统一数据格式处理是咋执行的
1、大概讲解
咱还是先大概讲解一下吧:统一数据返回和统一异常都是基于@ControllerAdvice这个注解实现的,通过研究@ControllerAdvice这个注解相关的源码可以知道他们的执行流程~
从DispatcherServlet类开始分析,他在初始化阶段,咱重点关注这三个方法:
initHandlerMappings(context);初始化处理器的
initHandlerAdapters(context);初始化适配器的
initHandlerExceptionResolvers(context);初始化异常处理器的
2、插播几条知识
(先插播一条信息吧,讲解一下前两种方法,还有处理器和适配器之间的关系,这对后面的讲解至关重要)
initHandlerMappings(context);初始化处理器的,点进去看看这个方法:
594行-535行是这个方法的核心,这是从spring的容器中获取bean的,获取什么类型的bean的呢?获取实现了HandlerMapping接口的bean,也就是获取很多个处理器对象,不同的处理器会处理不同的内容,比如RequestMappingHandler,它的存在,正是@RequestMapping注解生效的主要原因,当请求发出后,它会获取到url,并做一个映射,什么映射呢?url对应哪个controller的映射,但是处理器无法直接做出这些内容,所以要借助相应的适配器来做,在适配器里面,完成处理器想要做出的内容。
initHandlerAdapters(context);初始化适配器的,点进去看看~
这个方法里面,又会获取到实现了HandlerMapping接口的对象,也就是各种适配器,去帮助不同的处理器完成相应的内容~
好,讲解完毕,咱们开始正题~
3、具体讲解
initHandlerAdapters(context);在这个方法里面,会获取到所有使用@ControllerAdvice的对象,再进行下一步处理,这就是统一数据返回格式能够生效的主要原因。
spring在启动的时候,会进行一系列初始化的工作,其中包括异常模块的处理,就是在initHandlerAdapters(context);里面处理的,具体如何处理的呢?若异常产生后,在异常处理类中有多个处理器,首先会进行匹配,若匹配到多个处理器,会进行排序,根据什么规则排序呢?根据抛出的异常相对于声明出的异常参数的深度,最后选择一个深度最低的处理器,对异常进行处理。