1. 拦截器
引入
上个章节我们完成了强制登录的功能, 后端程序根据Session来判断用户是否登录, 但是实现⽅法是⽐较麻烦的
• 需要修改每个接⼝的处理逻辑
• 需要修改每个接⼝的返回结果
• 接⼝定义修改, 前端代码也需要跟着修改
有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 这⾥我们学习⼀种新的解决办法: 拦截
概念:
拦截器是Spring框架提供的核⼼功能之⼀, 主要用来拦截用户的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码.(一般拦截的是controller里面的)
也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也可以在⽤⼾请求前阻⽌其执⾏.
在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求, 判断Session中是否有登录用户的信息. 如果有就可以放行, 如果没有就进⾏拦截.
学习拦截器的使用
1> 定义拦截器(定义一下这个拦截器做什么工作,岗位职责)
2> 注册配置拦截器(设置拦截器,哪些拦截,哪些不拦截)
初步的体验如何使用
HandlerInterceptor,Hander表示处理器,Interceptor表示拦截器,在这个类里面我们后续会补充拦截的逻辑
源码分析
在这个代码中,我们对拦截器进行注册,实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法
我们来看看不同语句打印的时间是如何
拦截器详细解释
拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.我们在注册配置拦截器的时候, 通过 addPathPatterns() ⽅法指定要拦截哪些请求. 也可以通过excludePathPatterns() 指定不拦截哪些请求.
然后我们回到config.我们肯定不可以把所有的请求都排除掉,我们登录注册这种请求肯定要有的.
然后我们看看运行user/login之后有没有打印(发现确实是没有打印)
拦截器的调用流程
正常的调用
有了拦截器之后的调用:有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理
文字描述版本
1. 添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法,这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的⽅法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
2. controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据.
实现登录校验拦截器
HttpSession session = request.getSession(false)这行代码的参数所代表的意思
我们拦截器的内部逻辑实现
此时我们可以看见,除了登录页面其他页面都访问不进去
然后我们还要加一点,也就是判断没有登录之后我们要跳转到登录页面,此时我们需要修改前端代码,结果如下:我们访问book_list.html就直接跳转到登录页面了
日志解释
tomcat是一个web项目的容器,可以装n个项目,然后项目和项目之间使用context path来进行区分.但是spring把tomcat集成进来之后,tomcat就只能启动一个项目了,因此context path为空
具体url路径
关于servlet我们补充一下
Spring是基于servlet开发的,servlet的生命周期(可能是面试题)
init: 初始化九大组键
service: 当请求来的时候具体做的工作是什么(业务的具体逻辑,会来回调用,现在用controller来写)
destory: 销毁
拦截器源码分析:
DispatcherServlet源码
当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.DispatcherServlet,这个是一个调度servlet,调度整个spring,主流程.我们阅读源码就是这么一个过程,先找到主流程,然后根据单词猜意思.(连猜带过)
ctrl+alt+<-表示上一个刚刚看的,ctrl+alt+->表示下一次代码(来回可以切换)
看源码某个变量主流程怎么用,我们也可以debug,然后跑一下我们的程序,根据注释连蒙带猜,打日志的不看
源码分析
适配器模式
概念
适配器模式, 也叫包装器模式. 将⼀个类的接⼝,转换成期望的另⼀个接⼝, 适配器让原本接⼝不兼容的类可以合作⽆间.
简单来说就是⽬标类不能直接使用, 通过⼀个新类进⾏包装⼀下, 适配调用方使⽤. 把两个不兼容的接⼝通过⼀定的⽅式使之兼容.
⽐如下⾯两个接⼝, 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)
适配器模式角色
• Target: ⽬标接⼝ (可以是抽象类或接⼝), 客户希望直接⽤的接⼝
• Adaptee: 适配者, 但是与Target不兼容
• Adapter: 适配器类, 此模式的核⼼. 通过继承或者引⽤适配者的对象, 把适配者转为⽬标接⼝
• client: 需要使⽤适配器的对象
通过适配器类把适配者转换为用户要的目标接口
适配器模式的实现
适配器模式的应用场景
⼀般来说,适配器模式可以看作⼀种"补偿模式",用来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了
所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新的功能. ⽐如版本升级等.
注意:idea中要找某个类或者方法,可以连续按俩下shift,会弹出搜索框
Spring初始化模块
init初始化九大主键(这样我们才知道我们url对应的是哪个方法,把它交给谁来处理,主要还是适配器来进行处理,通过适配器找到目标类(adapter调用controller))
我们着重介绍三个
initHandlerMappings(context);处理器映射(我们客户端里面的url里面的/xx/xx,是一个字符串,映射到spring项目controller里面@RequestMapping("/xx)这个就是HandlerMapping),url->哪个controller里面的方法
initHandlerAdapters(context);处理器适配器(我们的spring是基于servlet实现的,不仅支持controller还支持servlet等,而我们的url对应的可能是controller可能是servlet,此时就需要一个适配器进行适配)
initHandlerExceptionResolvers(context);异常解析器
一些专业名词;
dispatch(调度),adapter(适配器),context(上下文),handler(处理器),Resovler(解析器),apply(应用/执行),setAttribute(设置属性),mapping(映射/路由),mulpart(文件相关),interceptor(拦截器),Chain(链)
service
2. 统⼀数据返回格式
引入
强制登录案例中, 我们共做了两部分⼯作
1. 通过Session来判断用户是否登录
2. 对后端返回数据进⾏封装, 告知前端处理的结果
后端统一返回结果
后端逻辑处理
Result.success(pageResult) 就是对返回数据进⾏了封装
拦截器帮我们实现了第⼀个功能, 接下来看SpringBoot对第⼆个功能如何⽀持
第二个功能的思路如图,但是如果我们每一个都改成Result类型,这是个重复性工作,因此我们提到了统一返回格式,我们将学习如何对数据进行统一的封装
具体使用
此时我们就要学习统一的数据返回格式
统⼀的数据返回格式使⽤ @ControllerAdvice 和 ResponseBodyAdvice 的⽅式实现 @ControllerAdvice 表⽰控制器通知类.这个body(表示请求的正文:date,content-type..)
如果设置为false,就不会对结果进行统一处理
添加类 ResponseAdvice , 实现 ResponseBodyAdvice 接⼝, 并在类上添加@ControllerAdvice 注解
我们对每个格式都进行处理(String必须单独处理,后续会详细解释),把String类型转换成Result类型需要一个信息转换器(messageConverters)
然后我们测试其他返回类型
如过不对Sting进行单独处理,会报错,类型不匹配,需要的是String类型,但是返回的是Result类型
阅读源码
具体的报错:类型不匹配
具体解释,我们读源码,这个是不加String类型的流程
加了String类型处理的流程
ctrl+alt+b进入方法实现,如果接口实现或者子类,就会直接跳转到子类或者实现类,f9下一个断点,f8下一步,f7进入方法
我们详细读一下这个代码
主要读hadlerMapping和Adapter
关于MessageConvert的补充: 我们的信息转换器有很多,那么是如何判断哪个转换器可以处理? (后续再补充)
统一数据返回的优点优点:
1. 方便前端程序员更好的接收和解析后端数据接⼝返回的数据
2. 降低前端程序员和后端程序员的沟通成本, 按照某个格式实现就可以了, 因为所有接⼝都是这样返回的.
3. 有利于项⽬统⼀数据的维护和修改.
4. 有利于后端技术部门的统⼀规范的标准制定, 不会出现稀奇古怪的返回内容.
3. 统一异常处理
引入
在图书管理系统里面,我们BookController里面是有异常处理的,但是有一些接口我并没有进行异常处理,这样的话很有可能会报错,会抛给调用方.(我们还是要把异常进行捕获),因此在这个程序中不管什么样的异常,只要能够捕获,就不要进行处理.(不管内部产生了什么错误,我们不能够让外部人员感知到,不能发送500,404等等信息,我们统一返回一个错误信息)
可以来看看b站404的处理
具体使用
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实的,@ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件.
以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类), 就返回⼀个 Result的对象, Result对象的设置参考 Result.fail(e.getMessage())
加上@ResponseBody和不加的返回结果,我们404不一定是url写错了,有可能是因为没有写注解.
进行测试,我们模拟制造异常(记得关闭拦截器和统一结果返回)
后端返回
我们也可以对异常进行细分(如果有我们写的异常捕获就会被那个捕获,如果没有具体写的异常捕获,那么久直接使用它的上一层捕获(Exception),优先子类捕获,子类捕获不到就父类进行捕获
另一种写法
阅读源码
@ControllerAdvice 源码分析统⼀数据返回和统⼀异常都是基于 @ControllerAdvice 注解来实现的, 通过分析@ControllerAdvice 的源码, 可以知道他们的执⾏流程.点击 @ControllerAdvice 实现.Advice可以这么理解(执行的具体逻辑是什么)
源码如下:
从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件, 这也就是为什么没有五⼤注解, ControllerAdvice 就⽣效的原因.
下⾯我们看看Spring是怎么实现的, 还是从 DispatcherServlet 的代码开始分析.DispatcherServlet 对象在创建时会初始化⼀系列的对象:
对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context) 和initHandlerExceptionResolvers(context) 这两个⽅法.
1> initHandlerAdapters(context)
initHandlerAdapters(context) ⽅法会取得所有实现了 HandlerAdapter 接⼝的bean并保存起来,其中有⼀个类型为 RequestMappingHandlerAdapter 的bean,这个bean.@RequestMapping 注解能起作⽤的关键,这个bean在应⽤启动过程中会获取所有被@ControllerAdvice 注解标注的bean对象, 并做进⼀步处理,关键代码如下:
这个⽅法在执⾏时会查找使⽤所有的 @ControllerAdvice 类,把 ResponseBodyAdvice 类放在容器中,当发⽣某个事件时,调⽤相应的 Advice ⽅法,⽐如返回数据前调⽤统⼀数据封装
2> initHandlerExceptionResolvers(context)
接下来看 DispatcherServlet 的 initHandlerExceptionResolvers(context) ⽅法,这个⽅法会取得所有实现了 HandlerExceptionResolver 接⼝的bean并保存起来,其中就有⼀个类型为 ExceptionHandlerExceptionResolver 的bean,这个bean在应⽤启动过程中会获取所有被 @ControllerAdvice 注解标注的bean对象做进⼀步处理,代码如下:
当Controller抛出异常时, DispatcherServlet 通过ExceptionHandlerExceptionResolver 来解析异常,而ExceptionHandlerExceptionResolver ⼜通过 ExceptionHandlerMethodResolver来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适⽤的@ExceptionHandler标注的⽅法是这⾥:
ExceptionHanlderExceptioResolver和@ExceptionHandler对应的上.我们这里介绍一下afterPropertiesSet()方法.(后置处理器)Spring一开始先根据五大注解加载Bean,在加载完之后,再从那些bean里面找HandlerExceptionResolver的实现,然后执行一些额外的初始化.bean的初始化执行后的方法就是afterPropertiesSet()
判断不同的类型,然后加到不同的地方去
整体流程
然后我们对异常处理,它怎么分辨是哪个异常被怎么处理.这个操作进行详细解释
上面的统一结果的返回都是后端代码,我们现在对前端代码也进行修改
修改登录接口
总结:
1. 拦截器的实现主要分两部分: 1. 定义拦截器(实现HandlerInterceptor 接⼝) 2. 配置拦截器
2. 统⼀数据返回格式通过@ControllerAdvice + ResponseBodyAdvice 来实现
3. 统⼀异常处理使⽤@ControllerAdvice + @ResponseBody+@ExceptionHandler 来实现, 并且可以分异常来处理
4. 学习了DispatcherServlet的⼀些源码.