1. 拦截器(Interceptor)
我们完成了强制登录的功能, 后端程序根据Session来判断用户是否登录, 但是实现⽅法是比较麻烦的。
所需要处理的内容:
• 需要修改每个接⼝的处理逻辑
• 需要修改每个接⼝的返回结果
• 接⼝定义修改, 前端代码也需要跟着修改很麻烦!!!
1.1 什么是拦截器
拦截器是Spring框架提供的核心功能之⼀, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码
也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也可以在⽤⼾请求前阻止其执行.
在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息.如果有就可以放行,如果没有就进行拦截
1.2 拦截器的基本使用
下⾯我们先来学习下拦截器的基本使⽤.
拦截器的使⽤步骤分为两步:
1. 定义拦截器
2. 注册配置拦截器
1.2.1 ⾃定义拦截器
实现HandlerInterceptor接⼝,并重写其所有⽅法
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⽅法
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 定义拦截器
http状态码401: UnauthorizedIndicates that authentication is required and was either not provided or has failed. If therequest already included authorization credentials, then the 401 status code indicates thatthose credentials were not accepted.中⽂解释: 未经过认证. 指⽰⾝份验证是必需的, 没有提供⾝份验证或⾝份验证失败. 如果请求已经包含授权凭据,那么401状态码表⽰不接受这些凭据。
2.2 注册配置拦截器
同时,我们调用方法时发现可以传递List<>
故上述代码也可以改成
//asList->将数组转成list //使用该方法 可以将一个变长参数或者数组转换成List
删除之前的登录校验代码
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 中使⽤了适配器模式
适配器模式定义
适配器模式, 也叫包装器模式.
将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝, 适配器让原本接⼝不兼容的类可以合作⽆间.
简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤. 把两个不兼容的接⼝通过⼀定的⽅式使之兼容.
⽐如下⾯两个接⼝, 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)
不兼容的两个接口
4. 统⼀数据返回格式
统⼀的数据返回格式使⽤ @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);
}
}
// 获取执⾏的类Class<?> declaringClass = returnType.getMethod().getDeclaringClass();// 获取执⾏的⽅法Method method = returnType.getMethod();
• beforeBodyWrite⽅法: 对response⽅法进⾏具体操作处理
4.1 测试
测试
添加统⼀数据返回格式之后:
加此注解
4.2 存在问题
查看⽇志, ⽇志报错
4.2.1 测试方法
发现只有返回结果为String类型时才有这种错误发⽣!!!
4.2.2 解决⽅案
@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 案例代码修改
5.统⼀异常处理
类名, ⽅法名和返回值可以⾃定义, 重要的是注解接⼝返回为数据时, 需要加 @ResponseBody 注解
我们可以针对不同的异常, 返回不同的结果
测试结果
6.@ControllerAdvice 源码分析
- 从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件, 这也就是为什么没有五 ⼤注解, ControllerAdvice 就⽣效的原因.
- 下⾯我们看看Spring是怎么实现的, 还是从 DispatcherServlet 的代码开始分析.
- DispatcherServlet 对象在创建时会初始化⼀系列的对象
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);
}
//...
}
6.1. initHandlerAdapters(context)
6.2. initHandlerExceptionResolvers(context)
7. 案例代码
7.1 登录页面
登录界⾯没有拦截, 只是返回结果发⽣了变化, 所以只需要根据返回结果修改对应代码即可
7.2图书列表
7.3 其他
7.4 测试
先后端接口测试后前端测试
//修改时遇到的问题
//统一返回String类型出现错误
//改正1.前端处理
//改正2.接口设置返回类型