项目4-图书管理系统2+统一功能处理

1. 拦截器(Interceptor)

我们完成了强制登录的功能, 后端程序根据Session来判断用户是否登录, 但是实现⽅法是比较麻烦的。

所需要处理的内容:

• 需要修改每个接⼝的处理逻辑

• 需要修改每个接⼝的返回结果

• 接⼝定义修改, 前端代码也需要跟着修改

很麻烦!!!

1.1 什么是拦截器

拦截器是Spring框架提供的核心功能之⼀, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码

也就是说, 允许开发⼈员提前预定义⼀些逻辑 , 在⽤⼾的请求响应前后执⾏ . 也可以在⽤⼾请求前阻止其执行 .

在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息.如果有就可以放行,如果没有就进行拦截

1.2 拦截器的基本使用

下⾯我们先来学习下拦截器的基本使⽤.

拦截器的使⽤步骤分为两步:

  1. 定义拦截器

  2. 注册配置拦截器

1.2.1 ⾃定义拦截器

实现HandlerInterceptor接⼝,并重写其所有⽅法

java 复制代码
package com.example.demo.component;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("LoginInterceptor ⽬标⽅法执⾏前执⾏..");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("LoginInterceptor ⽬标⽅法执⾏后执⾏");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("LoginInterceptor 视图渲染完毕后执⾏,最后执⾏");
    }
}

• preHandle()⽅法:⽬标⽅法执⾏前执⾏. 返回true: 继续执⾏后续操作; 返回false: 中断后续操作.

• postHandle()⽅法:⽬标⽅法执⾏后执⾏

• afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图, 暂不了解)

1.2.2 注册配置拦截器

实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法

java 复制代码
package com.example.demo.configuration;

import com.example.demo.component.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求)
    }
}

启动服务, 试试访问任意请求, 观察后端⽇志

可以看到preHandle ⽅法执⾏之后就放⾏了, 开始执⾏⽬标⽅法, ⽬标⽅法执⾏完成之后执⾏

postHandle和afterCompletion⽅法

我们把拦截器中preHandle⽅法的返回值改为false, 再观察运⾏结果

可以看到, 拦截器拦截了请求, 没有进⾏响应.

1.3 拦截器详解

拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。

拦截器的使⽤细节我们主要介绍两个部分:

  1. 拦截器的拦截路径配置

  2. 拦截器实现原理

1.3.1 拦截路径

拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.

我们在注册配置拦截器的时候,

通过 addPathPatterns() ⽅法指定要拦截哪些请求.(即就是让哪些地方的拦截操作生效)【上述代码中, 我们配置的是 /** , 表⽰拦截所有的请求】

也可以通过excludePathPatterns() 指定不拦截哪些请求

⽐如⽤⼾登录校验, 我们希望可以对除了登录之外所有的路径⽣效

在拦截器中除了可以设置 /** 拦截所有资源外,还有⼀些常⻅拦截路径设置:

|------------|--------------|---------------------------------------------------------|
| 拦截路径 | 含义 | 举例 |
| /* | ⼀级路径 | 能匹配/user,/book,/login,不能匹配 /user/login |
| /** | 任意级路径 | 能匹配/user,/user/login,/user/reg |
| /book/* | /book下的⼀级路径 | 能匹配/book/addBook,不能匹配/book/addBook/1,/book |
| /book/** | /book下的任意级路径 | 能匹配/book,/book/addBook,/book/addBook/2,不能匹 配/user/login |

//后缀名被拦截:*.html,访问后缀名为html资源时,过滤器都会被执行

以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件, JS 和 CSS 等⽂件)

1.3.2 拦截器执行流程

正常的调⽤顺序:

有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图

1. 添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法,这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的方法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
2. controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及 afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据.

2.登录校验

学习拦截器的基本操作之后,接下来我们需要完成最后⼀步操作:通过拦截器来完成图书管理系统中 的登录校验功能

2.1 定义拦截器

从session中获取⽤⼾信息, 如果session中不存在, 则返回false,并设置http状态码为401, 否则返回true.

http状态码401: Unauthorized
Indicates that authentication is required and was either not provided or has failed. If the
request already included authorization credentials, then the 401 status code indicates that
those credentials were not accepted.
中⽂解释: 未经过认证. 指⽰⾝份验证是必需的, 没有提供⾝份验证或⾝份验证失败. 如果请求已经包含授权凭据,那么401状态码表⽰不接受这些凭据。

2.2 注册配置拦截器

同时,我们调用方法时发现可以传递List<>

故上述代码也可以改成

复制代码
//asList->将数组转成list
//使用该方法 可以将一个变长参数或者数组转换成List

删除之前的登录校验代码


运行程序, 通过Postman进⾏测试:

  1. 查看图书列表

  2. 登录之后再次进行图书列表的查看

3.DispatcherServlet 源码分析(dispatch派遣)

当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.

所有请求都会先进到DispatcherServlet,执⾏doDispatch 调度⽅法.

如果有拦截器, 会先执⾏拦截器preHandle() ⽅法的代码, 如果 preHandle() 返回true, 继续访问controller中的⽅法. controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 和 afterCompletion() ,返回给DispatcherServlet,最终给浏览器响应数据.

3.1 初始化

DispatcherServlet的初始化⽅法 init() 在其⽗类 HttpServletBean 中实现的.

主要作⽤是加载 web.xml 中 DispatcherServlet 的 配置, 并调⽤⼦类的初始化.

web.xml是web项⽬的配置⽂件,⼀般的web⼯程都会⽤到web.xml来配置,主要⽤来配置

Listener,Filter,Servlet等, Spring框架从3.1版本开始⽀持Servlet3.0, 并且从3.2版本开始通过配置DispatcherServlet, 实现不再使⽤web.xml

3.2 处理请求

DispatcherServlet 接收到请求后, 执⾏doDispatch 调度⽅法, 再将请求转给Controller.

我们来看doDispatch ⽅法的具体实现

查看源码方式:

ctrl+N全局搜索类,ctrl+R在一个类中搜索方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            try {
                ModelAndView mv = null;
                Object dispatchException = null;

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }

                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    String method = request.getMethod();
                    boolean isGet = HttpMethod.GET.matches(method);
                    if (isGet || HttpMethod.HEAD.matches(method)) {
                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                            return;
                        }
                    }

                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }

                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }

                    this.applyDefaultViewName(processedRequest, mv);
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                } catch (Exception var20) {
                    dispatchException = var20;
                } catch (Throwable var21) {
                    dispatchException = new NestedServletException("Handler dispatch failed", var21);
                }

                this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
            } catch (Exception var22) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
            } catch (Throwable var23) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
            }

        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            } else if (multipartRequestParsed) {
                this.cleanupMultipart(processedRequest);
            }

        }
    }

HandlerAdapter 在 Spring MVC 中使⽤了适配器模式

适配器模式, 也叫包装器模式. 简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤.

把两个不兼容的接⼝通过⼀定的⽅式使之兼容.

HandlerAdapter 主要⽤于⽀持不同类型的处理器(如 Controller、HttpRequestHandler 或者

Servlet 等),让它们能够适配统⼀的请求处理流程。这样,Spring MVC 可以通过⼀个统⼀的接⼝来处理来⾃各种处理器的请求

4.3 适配器模式

HandlerAdapter 在 Spring MVC 中使⽤了适配器模式
适配器模式定义

适配器模式, 也叫包装器模式.

将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝, 适配器让原本接⼝不兼容的类可以合作⽆间.

简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤. 把两个不兼容的接⼝通过⼀定的⽅式使之兼容.

⽐如下⾯两个接⼝, 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)

不兼容的两个接口


通过适配器的⽅式, 兼容的A,B接口
适配器模式⻆⾊
• Target: ⽬标接⼝ (可以是抽象类或接⼝), 客⼾希望直接⽤的接⼝
• Adaptee: 适配者, 但是与Target不兼容
• Adapter: 适配器类, 此模式的核⼼. 通过继承或者引⽤适配者的对象, 把适配者转为⽬标接⼝
• client: 需要使⽤适配器的对象
适配器模式应⽤场景
⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之 举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新 的功能. ⽐如版本升级等.

4. 统⼀数据返回格式

强制登录案例中, 我们共做了两部分⼯作

  1. 通过Session来判断⽤⼾是否登录
  2. 对后端返回数据进⾏封装, 告知前端处理的结果
    拦截器帮我们实现了第⼀个功能, 接下来看SpringBoot对第⼆个功能如何⽀持

统⼀的数据返回格式使⽤ @ControllerAdvice 和 ResponseBodyAdvice 的⽅式实现
@ControllerAdvice 表⽰控制器通知类
添加类 ResponseAdvice , 实现 ResponseBodyAdvice 接⼝, 并在类上添加@ControllerAdvice 注解

public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body);
    }
}

supports⽅法:
判断是否要执⾏beforeBodyWrite⽅法. true为执⾏, false不执⾏. 通过该⽅法可以选择哪些类或哪些⽅法的response要进⾏处理, 其他的不进⾏处理.
从returnType获取类名和⽅法名

// 获取执⾏的类
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
// 获取执⾏的⽅法
Method method = returnType.getMethod();

• beforeBodyWrite⽅法: 对response⽅法进⾏具体操作处理

4.1 测试

测试
添加统⼀数据返回格式之前:

添加统⼀数据返回格式之后:

加此注解

4.2 存在问题


结果显⽰(500), 发⽣内部错误
查看数据库, 发现数据操作成功

查看⽇志, ⽇志报错

4.2.1 测试方法

多测试⼏种不同的返回结果
测试代码:

测试结果

发现只有返回结果为String类型时才有这种错误发⽣!!!

4.2.2 解决⽅案

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    private static ObjectMapper mapper=new ObjectMapper();
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if(body instanceof String){//如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化
            try {
                return mapper.writeValueAsString(Result.success(body));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return Result.success(body);
    }
}

成功!!!

4.2.3 原因分析

  • SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter (从先后顺序排列分别为

  • ByteArrayHttpMessageConverter , StringHttpMessageConverter , SourceHttpMessageConverter , SourceHttpMessageConverter , AllEncompassingFormHttpMessageConverter )

  • 其中AllEncompassingFormHttpMessageConverter 会根据项⽬依赖情况 添加对应的

  • HttpMessageConverter

  • 在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到messageConverters 链的末尾.

  • Spring会根据返回的数据类型, 从 messageConverters 链选择合适的 HttpMessageConverter .

  • 当返回的数据是⾮字符串时, 使⽤的 MappingJackson2HttpMessageConverter 写⼊返回对象.

  • 当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为 StringHttpMessageConverter 可以使⽤.

    • 在 ((HttpMessageConverter) converter).write(body, selectedMediaType,
      outputMessage) 的处理中, 调⽤⽗类的write⽅法
  • 由于 StringHttpMessageConverter 重写了addDefaultHeaders⽅法, 所以会执⾏⼦类的⽅法

  • 然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String, 此时t为Result类型, 所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常

4.2.4 案例代码修改

如果⼀些⽅法返回的结果已经是Result类型了, 那就直接返回Result类型的结果即可

5.统⼀异常处理

统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的, @ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件

类名, ⽅法名和返回值可以⾃定义, 重要的是注解
接⼝返回为数据时, 需要加 @ResponseBody 注解
以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类), 就返回⼀个 Result的对象, Result 对象的设置参考 Result.fail(e.getMessage()

我们可以针对不同的异常, 返回不同的结果

测试结果


当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配

6.@ControllerAdvice 源码分析

统⼀数据返回和统⼀异常都是基于 @ControllerAdvice 注解来实现的, 通过分析@ControllerAdvice 的源码, 可以知道他们的执⾏流程.

  • 从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件, 这也就是为什么没有五 ⼤注解, ControllerAdvice 就⽣效的原因.
  • 下⾯我们看看Spring是怎么实现的, 还是从 DispatcherServlet 的代码开始分析.
  • DispatcherServlet 对象在创建时会初始化⼀系列的对象
java 复制代码
public class DispatcherServlet extends FrameworkServlet {
 //...
 @Override
 protected void onRefresh(ApplicationContext context) {
 initStrategies(context);
 }
 
 /**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further 
strategy objects.
 */
 protected void initStrategies(ApplicationContext context) {
 initMultipartResolver(context);
 initLocaleResolver(context);
 initThemeResolver(context);
 initHandlerMappings(context);
 initHandlerAdapters(context);
 initHandlerExceptionResolvers(context);
 initRequestToViewNameTranslator(context);
 initViewResolvers(context);
 initFlashMapManager(context);
 }
 //...
}

对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context) 和
initHandlerExceptionResolvers(context) 这两个⽅法.

6.1. initHandlerAdapters(context)

initHandlerAdapters(context) ⽅法会取得所有实现了 HandlerAdapter 接⼝的bean并
保存起来,其中有⼀个类型为 RequestMappingHandlerAdapter 的bean,这个bean就是
@RequestMapping 注解能起作⽤的关键,这个bean在应⽤启动过程中会获取所有被@ControllerAdvice 注解标注的bean对象, 并做进⼀步处理。

6.2. initHandlerExceptionResolvers(context)

接下来看 DispatcherServlet 的 initHandlerExceptionResolvers(context) ⽅法,
这个⽅法会取得所有实现了 HandlerExceptionResolver 接⼝的bean并保存起来,其中就有⼀
个类型为 ExceptionHandlerExceptionResolver 的bean,这个bean在应⽤启动过程中会获
取所有被 @ControllerAdvice 注解标注的bean对象做进⼀步处理

7. 案例代码

通过上⾯统⼀功能的添加, 我们后端的接⼝已经发⽣了变化(后端返回的数据格式统⼀变成了Result类型), 所以我们需要对前端代码进⾏修改

7.1 登录页面

登录界⾯没有拦截, 只是返回结果发⽣了变化, 所以只需要根据返回结果修改对应代码即可
登录结果代码修改
tips:可以根据返回的东西更新前端返回代码

7.2图书列表

针对图书列表⻚有两处变化

  1. 拦截器进⾏了强制登录校验, 如果校验失败, 则http状态码返回401, 此时会⾛ajax的error逻辑处理
  2. 接⼝返回结果发⽣了变化
    图书列表代码修改

7.3 其他

参考图书列表, 对删除图书, 批量删除图书,添加图书, 修改图书接⼝添加⽤⼾强制登录以及统⼀格式返回的逻辑处理

7.4 测试

先后端接口测试后前端测试

//修改时遇到的问题

//统一返回String类型出现错误

//改正1.前端处理

//改正2.接口设置返回类型

相关推荐
信徒_几秒前
常用设计模式
java·单例模式·设计模式
神仙别闹6 分钟前
基于C#实现的(WinForm)模拟操作系统文件管理系统
java·git·ffmpeg
小爬虫程序猿7 分钟前
利用Java爬虫速卖通按关键字搜索AliExpress商品
java·开发语言·爬虫
组合缺一12 分钟前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·后端·spring·solon
程序猿零零漆14 分钟前
SpringCloud 系列教程:微服务的未来(二)Mybatis-Plus的条件构造器、自定义SQL、Service接口基本用法
java·spring cloud·mybatis-plus
猿来入此小猿16 分钟前
基于SpringBoot在线音乐系统平台功能实现十二
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
愤怒的代码30 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
带多刺的玫瑰30 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
栗豆包1 小时前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
夜半被帅醒1 小时前
MySQL 数据库优化详解【Java数据库调优】
java·数据库·mysql