主要总结了一下内容:
1.掌握拦截器的使用及其原理
2.学习统一数据返回格式和统一异常的操作
一.拦截器
1.拦截器快速入门
1.什么是拦截器?
拦截器是Spring框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码.
2.拦截器的基本使用:
1.定义拦截器
2.注册配置拦截器
自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor 目标方法执行前执行");
return false;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor 目标方法执行后执行");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
log.info("LoginInterceptor 视图渲染完毕后执行");
}
}
preHandle()方法:目标方法执行前执行. 返回true:继续执行后续操作;返回false:中断后续操作.
postHandle()方法:目标方法执行后执行
afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图,暂不了解
注册配置拦截器:实现webMvcConfigurer接口,并重写addInterceptors方法
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**");
}
}
2.拦截器详解
拦截器的使用细节我们主要了解两个部分:
1.拦截器的拦截路径配置 2.拦截器的实现原理
1.拦截器的拦截路径配置
拦截路径是指我们定义的这个拦截器,对哪些请求生效
我们在注册配置拦截器的时候,通过addPathPatterns()方法指定要拦截哪些请求,也可以通过excludePathPatterns()指定不拦截哪些请求
2.拦截器的实现原理
拦截器的执行流程
正常的调用顺序

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

-
添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住.执行 preHandle()方法,这个方法需要返回一个布尔类型的值.如果返回true,就表示放行本次操作,继续访问controller中的方法.如果返回false,则不会放行(controller中的方法也不会执行).
-
controller当中的方法执行完毕后,再回过来执行postHandle()这个方法以及afterCompletion()方法,执行完毕之后,最终给浏览器响应数据.
3.DispatcherServlet源码分析
观察我们的服务启动日志:

当Tomcat启动之后,有一个核心的类DispatcherServlet,他来控制程序的执行顺序
所有的请求都会先进到DIspatcherServlet,执行doDispatch调度方法,如果有拦截器,会先执行拦截器preHandle()方法的代码大,如果preHandle()方法返回true,继续访问controller中的方法,controller中的方法执行完毕之后,再回过来执行postHandle()和afterCompleion(),返回给DispatcherServlet,最终给浏览器响应数据

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
init()具体代码如下
java
public final void init() throws ServletException {
// ServletConfigPropertyValues 是静态内部类,使用 ServletConfig 获取web.xml 中配置的参数
PropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
//使用BeanWrapper来构造DIspatcherServlet
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
this.initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
} catch (BeansException var4) {
if (this.logger.isErrorEnabled()) {
this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
}
throw var4;
}
}
//让子类实现的方法,这种在父类定义在子类实现的方式叫做模板方法模式
this.initServletBean();
}
在 HttpServletBean中init()调用了initServletBean(),他是在FrameworkServlet类中的实现的,主要作用是建立webApplicationContext容器(有时候也叫做上下文),并加载SpringMVC配置文件中定义的Bean到该容器中,最后该容器添加到ServletContext中,下面是initServletBean()的具体代码
java
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
//创建ApplicationContext容器
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
此处打印出来的日志,也正是控制台打印出来的日志

源码追踪技巧:
1.在阅读框架源码的时候,一定要抓住关键点,找到核心流程
2.切忌从头到尾一行一行代码去看,一个方法的去研究,一定要找到关键流程,抓住关键点,先在宏观上
对整个流程或者整个原理有一个认识,有精力再去研究其中的细节.
初始化web容器的过程中,会通过onRefresh来初始化SpringMVC的容器
在initStrategies()中进行9大组件的初始化,如果没有配置相应的组件,就使用默认定义的组件
2.处理请求(核心)
DispatcherServlet接收到请求后,执行doDispatch调度方法,再将请求转发给controller
我们来看doDistpatch方法的具体实现
java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 获取执行链
//遍历所有的HandlerMapping找到与请求相应的Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 获取适配器
//遍历所有的HandlerAdapter,找到可以处理该Hander的HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
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;
}
}
//3.执行拦截器preHandler方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 4.执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
//5.执行拦截器postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}
//6.处理视图,处理之后执行拦截器afterCompletion方法
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//7.执行拦截器afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
cleanupMultipart(processedRequest);
}
}
}
}
HandlerAdapter 在Spring MVC中使用了适配器模式,下面详细再介绍
适配器模式,也叫包装器模式.简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配
调用方使用.
把两个不兼容的接口通过一定的方式使之兼容.
HandlerAdapter 主要用于支持不同类型的处理器(如Controller、HttpRequestHandler或者Servlet等),让它们能够适配统一的请求处理流程。这样,Spring MVC可以通过一个统一的接口来处理来自各种处理器的请求.
从上述源码我们可以看出在执行controller之前,会先调用预处理方法applyPreHandle,而applyPreHandle方法实现的源码如下:
java
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
//获取项目中使用的HandlerInterceptor
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
在applyPreHandle 中会获取所有的拦截器Handler Interceptor,并执行拦截器中的preHandle方法,这样就会咱们前面定义的拦截器对应上了
如果拦截器返回true,整个发放就返回true,继续执行后逻辑处理
如果返回false,则中断后续操作
3.适配器模式
HandlerAdapter在SpringMVC中使用了适配器模式
适配器模式定义
适配器模式也叫包装器模式。将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。
简单来说,就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方法使用,把两个不兼容的接口通过一定的方式使之兼容
适配器模式角色
Target:目标接口(可以是抽象类或接口),客户希望直接用的接口
Adaptee:适配者,但是与target不兼容
Adapter:适配器类,此模式的核心,通过继承或者引用适配器的对象,把适配器转为目标接口
client:需要使用适配器的对象
适配器模式的实现
前面学习的slf4j就使用了适配器模式,slf4j提供了一系列打印日志的api,底层调用的是log4j或者logback来打印日志,我们作为调用者,只需要调用slf4j的api就行
java
/**
* slf4j接口
*/
interface Slf4jApi{
void log(String message);
}
/**
* log4j接口
*/
class Log4j{
void log4jLog(String message){
System.out.println("Log4j打印:"+message);
}
}
/**
* slf4j和log4j的适配器
*/
class Slf4jLog4Adapter implements Slf4jApi{
private Log4j log4j;
public Slf4jLog4Adapter(Log4j log4j){
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jLog(message);
}
}
public class Slf4jDemo {
public static void main(String[] args) {
Slf4jApi slf4jApi = new Slf4jLog4Adapter(new Log4j());
slf4jApi.log("使用log4j打印日志");
}
}
可以看出,我们不需要改变log4j的api,只需要通过适配器转换,就可以更换日志框架,保障系统的平稳运行
适配器模式的应用场景:
一般来说,适配器模式可以看作一种"补偿 模式",用来补救设计上的缺陷,应用这种模式一般是"无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不选哟使用适配器模式了
所以适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以服用原有代码实现新的功能,比如版本升级等。
二.统一数据返回格式
统一的数据返回格式使用@ControllerAdvice和ResponseBodyAdvice的方式实现@ControllerAdvice表示控制器通知类添加类ResponseAdvice,实现ResponseBodyAdvice接口,并且类上添加@ControllerAdvice注解
java
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return false;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return null;
}
}
supports方法:判断是否要执行beforeBodyWrite方法,true执行,false不执行,通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理
beforeBodyWrite:对response方法进行具体操作处理
然后在测试的过程中,我们会发现当返回结果为string类型时会发生错误"Result cannot be cast to java.lang.string
这一现象的原因分析:
SpringMVC默认会注册一些自带的HttpMessageConverter(从先后顺序排列分别为 ByteArrayHttpMessageConverter ,StringHttpMessageConverter , 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"的异常
统一结果返回的优点:
1.方便前端更好的接受和解析后端返回的数据
2.降低前后端沟通成本
3.有利于项目统一数据的维护和修改
4.有利于后端技术部门的统一规范的标准指定
三.统一异常处理
统一异常处理使用的是@ControllerAdvice+@ExceptionHandler来实现的,
@ControllerAdvice表示控制器通知类,@Exception Handler是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
@RestControllerAdvice = @ControllerAdvice + @ResponseBody