目录
SpringBoot 统⼀功能处理
- 掌握拦截器的使⽤, 及其原理
- 学习统⼀数据返回格式和统⼀异常处理的操作
- 了解⼀些Spring的源码
拦截器
之前我们完成了强制登录的功能, 后端程序根据Session来判断⽤⼾是否登录, 但是实现⽅法是⽐较⿇烦的
- 需要修改每个接⼝的处理逻辑
- 需要修改每个接⼝的返回结果
- 接⼝定义修改, 前端代码也需要跟着修改
有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 这⾥⼀种新的解决办法: 拦截器
拦截器快速⼊⻔
什么是拦截器?
拦截器是Spring框架提供的核⼼功能之⼀, 主要⽤来拦截⽤⼾的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码.
拦截器的作用维度:URL
也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也可以在⽤⼾请求前阻⽌其执⾏.
在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求, 判断Session中是否有登录⽤⼾的信息. 如果有就可以放⾏, 如果没有就进⾏拦截.
⽐如我们去银⾏办理业务, 在办理业务前后, 就可以加⼀些拦截操作
办理业务之前, 先取号, 如果带⾝份证了就取号成功
业务办理结束, 给业务办理⼈员的服务进⾏评价.
这些就是"拦截器"做的⼯作
拦截器的基本使⽤:
拦截器的使⽤步骤分为两步:
- 定义拦截器
- 注册配置拦截器
⾃定义拦截器:实现HandlerInterceptor接⼝,并重写其所有⽅法
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
- **preHandle()**⽅法:⽬标⽅法执⾏前执⾏. 返回true: 继续执⾏后续操作; 返回false: 中断后续操作.
- **postHandle()**⽅法:⽬标⽅法执⾏后执⾏
- afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图, 暂不了解)
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("目标方法执行前");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("目标方法执行后");
}
}
注册配置拦截器:实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**");// /**表示给所有方法添加拦截器
}
}
启动服务, 试试访问任意请求, 观察后端⽇志
可以看到 preHandle ⽅法执⾏之后就放⾏了, 开始执⾏⽬标⽅法, ⽬标⽅法执⾏完成之后执⾏postHandle和afterCompletion⽅法.
我们把拦截器中preHandle⽅法的返回值改为false, 再观察运⾏结果
可以看到, 拦截器拦截了请求, 没有进⾏响应
拦截器详解
拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。拦截器的使⽤细节我们主要介绍两个部分:
- 拦截器的拦截路径配置
- 拦截器实现原理
拦截路径
拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.
我们在注册配置拦截器的时候, 通过 addPathPatterns()
⽅法指定要拦截哪些请求. 也可以通过excludePathPatterns()
指定不拦截哪些请求.
上述代码中, 我们配置的是
/**
, 表⽰拦截所有的请求.
⽐如⽤⼾登录校验, 我们希望可以对除了登录之外所有的路径⽣效.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**");// /**表示给所有方法添加拦截器
.excludePathPatterns("/user/login");//设置拦截器拦截的请求路径
}
}
在拦截器中除了可以设置 /**
拦截所有资源外,还有⼀些常⻅拦截路径设置:
拦截路径 | 含义 | 举例 |
---|---|---|
/* | ⼀级路径 | 能匹配/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 |
以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件, JS 和 CSS 等⽂件).
拦截器执⾏流程
正常的调⽤顺序:
有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图
-
添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏
preHandle()
⽅法,这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的⽅法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏). -
controller当中的⽅法执⾏完毕后,再回过来执⾏
postHandle()
这个⽅法以及afterCompletion()
⽅法,执⾏完毕之后,最终给浏览器响应数据.
登录校验
学习拦截器的基本操作之后,接下来我们需要完成最后⼀步操作:通过拦截器来完成图书管理系统中的登录校验功能
定义拦截器
从session中获取⽤⼾信息, 如果session中不存在, 则返回false,并设置http状态码为401, 否则返回true.
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("登录拦截器校验...");
//返回true表示放行,返回false表示拦截
//检验用户是否登录
HttpSession session = request.getSession(true);//true表示没有session就创建一个,false表示没有就直接返回
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
if (userInfo != null && userInfo.getId() >= 0) {
return true;//放行
}
response.setStatus(401);//401表示未认证登录
return false;//拦截
}
}
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状态码表⽰不接受这些凭据。
注册配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
//包含一些不应该被拦截的的URL路径
private static List<String> excludePath = Arrays.asList(
"/user/login",//排除这个特定的路径
//因为我们写的不是完全的前后端分离
//下面是为了拦截前端部分的静态资源
"/css/**",
"/js/**",
"/pic/**",
"/**/*.html");
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)//添加了拦截器
.addPathPatterns("/**")// /**表示给所有方法添加拦截器,即匹配所有路径
.excludePathPatterns(excludePath);
}
}
删除之前的登录校验代码
@RequestMapping("/getBookListByPage")
//为了方便更好拓展,最好返回结果也是一个对象
public Result getBookListByPage(PageRequest pageRequest, HttpSession session) {
log.info("查询翻页信息,pageRequest:{}", pageRequest);
用户登录校验
//UserInfo userInfo= (UserInfo) session.getAttribute("session_user_key");
//if(userInfo==null||userInfo.getId()<=0||"".equals(userInfo.getUserName())){
// //用户未登录
// return Result.unLogin();
//}
//校验成功
if (pageRequest.getPageSize() < 0 || pageRequest.getCurrentPage() < 1) {
//每页显示条数为负或者当前页数不为正数则错误
return Result.fail("参数校验失败");
}
PageResult<BookInfo> bookInfoPageResult = null;
try {
bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest);
return Result.success(bookInfoPageResult);
} catch (Exception e) {
log.error("查询翻页信息错误,e:{}", e);
return Result.fail(e.getMessage());
}
}
运⾏程序, 通过Postman进⾏测试:
- 查看图书列表
http://127.0.0.1:8080/book/getBookListByPage
观察返回结果: http状态码401
也可以通过Fiddler抓包观察
- 登录
http://127.0.0.1:8080/user/login?name=admin&password=admin
- 再次查看图书列表
数据进⾏了返回
DispatcherServlet 源码分析(了解)
观察我们的服务启动⽇志:
当Spiring的Tomcat启动之后, 有⼀个核⼼的类 DispatcherServlet, 它来控制程序的执⾏顺序.
dispatcher:调度程序
servlet的生命周期
init
service
destroy
所有请求都会先进到 DispatcherServlet,执⾏ doDispatch 调度⽅法. 如果有拦截器, 会先执⾏拦截器preHandle()
⽅法的代码, 如果 preHandle()
返回true, 继续访问 controller 中的⽅法. controller 当中的⽅法执⾏完毕后,再回过来执⾏ postHandle()
和 afterCompletion()
,返回给 DispatcherServlet,最终给浏览器响应数据.
初始化(了解)
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() 具体代码如下:
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() 的具体代码:
protected final void initServletBean() throws ServletException {
this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
//创建ApplicationContext容器
this.webApplicationContext = this.initWebApplicationContext();
this.initFrameworkServlet();
} catch (RuntimeException | ServletException var4) {
this.logger.error("Context initialization failed", var4);
throw var4;
}
if (this.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";
this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
}
if (this.logger.isInfoEnabled()) {
this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
此处打印的⽇志, 也正是控制台打印出来的⽇志
源码跟踪技巧:
在阅读框架源码的时候, ⼀定要抓住关键点, 找到核⼼流程.
切忌从头到尾⼀⾏⼀⾏代码去看, ⼀个⽅法的去研究, ⼀定要找到关键流程, 抓住关键点, 先在宏观上对整个流程或者整个原理有⼀个认识, 有精⼒再去研究其中的细节.
初始化web容器的过程中, 会通过 onRefresh 来初始化SpringMVC的容器
protected WebApplicationContext initWebApplicationContext() {
//...
if (!this.refreshEventReceived) {
//初始化Spring MVC
synchronized(this.onRefreshMonitor) {
this.onRefresh(wac);
}
}
//...
return wac;
}
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
在initStrategies()中进⾏9⼤组件的初始化, 如果没有配置相应的组件,就使⽤默认定义的组件(在DispatcherServlet.properties中有配置默认的策略, ⼤致了解即可)
⽅法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的处理⽅式⼏乎都⼀样(1.2.3.7.8,9),从应⽤⽂中取出指定的Bean, 如果没有, 就使⽤默认的.
⽅法initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers的处理⽅式⼏乎都⼀样(4,5,6,这三个重要一点)
初始化⽂件上传解析器MultipartResolver:从应⽤上下⽂中获取名称为multipartResolver的Bean,如果没有名为multipartResolver的Bean,则没有提供上传⽂件的解析器
初始化区域解析器LocaleResolver:从应⽤上下⽂中获取名称为localeResolver的Bean,如果没有这个Bean,则默认使⽤AcceptHeaderLocaleResolver作为区域解析器
初始化主题解析器ThemeResolver:从应⽤上下⽂中获取名称为themeResolver的Bean,如果没有这个Bean,则默认使⽤FixedThemeResolver作为主题解析器
初始化处理器映射器HandlerMappings:处理器映射器作⽤,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx⽅法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进⾏排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器
初始化处理器适配器HandlerAdapter:作⽤是通过调⽤具体的⽅法来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进⾏排序;如果在ApplicationContext中没有发现处理器适配器,则不设置异常处理器,则默认SimpleControllerHandlerAdapter作为处理器适配器
初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进⾏排序;如果在ApplicationContext中没有发现异常处理器解析器,则不设置异常处理器
初始化RequestToViewNameTranslator:其作⽤是从Request中获取viewName,从ApplicationContext发现有viewNameTranslator的Bean,如果没有,则默认使⽤DefaultRequestToViewNameTranslator
初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolver的Bean,如果没有,则默认InternalResourceViewResolver作为视图解析器
初始化FlashMapManager:其作⽤是⽤于检索和保存FlashMap(保存从⼀个URL重定向到另⼀个URL时的参数信息),从ApplicationContext发现有flashMapManager的Bean,如果没有,则默认使⽤DefaultFlashMapManager
以下是ChatGPT的回答:
这个过程涉及到加载和配置
DispatcherServlet
以及初始化与之相关的一系列Spring MVC组件。下面是对这个过程的分析和解释:
DispatcherServlet
的初始化
DispatcherServlet
的初始化是在其父类HttpServletBean
的init()
方法中完成的。这个方法的关键任务是从web.xml
或者通过编程式配置加载DispatcherServlet
的配置,并调用子类(如FrameworkServlet
和DispatcherServlet
)的初始化方法。1.
HttpServletBean.init()
- 加载Servlet配置:使用
ServletConfigPropertyValues
从ServletConfig
中读取配置,这可能包括对Spring MVC组件的定制配置。- 属性设置:通过
BeanWrapper
为Servlet设置属性,这包括了一些自定义编辑器和资源加载器的设置。2.
FrameworkServlet.initServletBean()
- 日志记录:记录初始化开始和完成的时间,这对于调试和监控是有帮助的。
- WebApplicationContext初始化:通过调用
initWebApplicationContext()
来建立和配置Web应用上下文。这个上下文是Spring MVC框架中所有组件交互的基础。- 异常处理:处理在初始化过程中可能发生的异常。
WebApplicationContext
的建立和配置
WebApplicationContext
是Spring MVC中非常核心的部分,它负责管理Spring MVC应用中的所有Bean。1.
FrameworkServlet.initWebApplicationContext()
- 获取根上下文:首先尝试从ServletContext中获取根上下文。
- 创建新的上下文:如果没有现成的上下文,创建一个新的。
- 上下文刷新:通过调用
onRefresh()
来完成上下文的初始化。初始化Spring MVC的9大组件
DispatcherServlet
在Spring MVC框架中起着中央调度器的作用。它负责初始化一系列关键组件,这些组件共同处理HTTP请求。1.
FrameworkServlet.onRefresh()
- 调用
initStrategies()
:这个方法负责初始化Spring MVC的九大组件。2.
DispatcherServlet.initStrategies()
- 组件初始化:包括文件上传解析器、区域解析器、主题解析器、处理器映射器、处理器适配器、异常处理器解析器、视图名称翻译器、视图解析器和FlashMap管理器的初始化。如果应用上下文中没有相应的Bean定义,则使用默认的策略。
总结
这个初始化过程体现了Spring框架的灵活性和可扩展性。通过这种方式,
DispatcherServlet
加载和配置自己的环境,并准备好处理传入的HTTP请求。每个组件都在整个请求处理流程中扮演着特定的角色,确保Spring MVC应用能够以高度可配置和可扩展的方式运行。
处理请求(核⼼)
DispatcherServlet 接收到请求后, 执⾏doDispatch 调度⽅法, 再将请求转给Controller.
我们来看 doDispatch ⽅法的具体实现
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;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//1. 获取执⾏链
//遍历所有的 HandlerMapping 找到与请求对应的Handler
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//2. 获取适配器
//遍历所有的 HandlerAdapter,找到可以处理该 Handler 的 HandlerAdapter
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;
}
}
//3. 执⾏拦截器preHandle⽅法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//4. 执⾏⽬标⽅法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
//5. 执⾏拦截器postHandle⽅法
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
//6. 处理视图, 处理之后执⾏拦截器afterCompletion⽅法
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
//7. 执⾏拦截器afterCompletion⽅法
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 可以通过⼀个统⼀的接⼝来处理来⾃各种处理器的请求.
从上述源码可以看出在开始执⾏ Controller 之前,会先调⽤ 预处理⽅法 applyPreHandle,⽽ applyPreHandle ⽅法的实现源码如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项⽬中使⽤的拦截器 HandlerInterceptor
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor
, 并执⾏拦截器中的 preHandle ⽅法,这样就会咱们前⾯定义的拦截器对应上了,如下图所⽰:
如果拦截器返回true, 整个发放就返回true, 继续执⾏后续逻辑处理
如果拦截器返回fasle, 则中断后续操作
DispatcherServlet.doDispatch()
方法的流程
- 处理多部分请求:
- 检查并处理请求是否为多部分(如文件上传)。
this.checkMultipart(request)
会在请求是多部分时返回一个包装后的请求对象。- 获取处理器(Handler):
- 通过
this.getHandler(processedRequest)
获取与请求相匹配的HandlerExecutionChain
(处理器执行链)。这个链包含了处理器(如Controller)和一系列拦截器。- 获取处理器适配器(Handler Adapter):
- 使用
this.getHandlerAdapter(mappedHandler.getHandler())
获取能够处理该请求的HandlerAdapter
。HandlerAdapter
负责调用实际的处理器(Controller)方法。- 执行拦截器的preHandle方法:
mappedHandler.applyPreHandle(processedRequest, response)
会执行拦截器链中所有拦截器的preHandle
方法。如果任何一个拦截器返回false
,则中断处理流程。- 执行目标方法:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
调用处理器(Controller)的方法,处理请求并返回ModelAndView
对象。- 执行拦截器的postHandle方法:
mappedHandler.applyPostHandle(processedRequest, response, mv)
在处理器方法执行后,ModelAndView返回前执行。- 处理视图和模型:
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
处理ModelAndView对象,渲染视图。- 执行拦截器的afterCompletion方法:
- 在请求处理完毕后,无论成功还是发生异常,都会执行
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22)
,调用拦截器的afterCompletion
方法。拦截器(HandlerInterceptor)的作用
拦截器在Spring MVC中用于在处理器(Controller)执行前后执行一些操作。它们通常用于日志记录、权限检查、事务处理等。
- preHandle:在处理器执行前调用。如果返回
false
,则中断执行链,后续的postHandle
和处理器方法将不会被执行。- postHandle:在处理器执行后,但在视图渲染前调用。
- afterCompletion:在请求完全结束后调用,用于清理资源。
总结
这个流程展示了Spring MVC如何处理一个HTTP请求:从确定处理器、适配器,到执行拦截器和处理器,再到渲染视图。这个过程中,拦截器的作用是在请求的前后提供了一个可插拔的方式来干预处理流程。这种架构提供了高度的灵活性和扩展性,允许开发者根据需要定制请求的处理过程。
适配器模式
HandlerAdapter 在 Spring MVC 中使⽤了适配器模式
适配器模式定义
适配器模式, 也叫包装器模式. 将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝, 适配器让原本接⼝不兼容的类可以合作⽆间.
简单来说就是⽬标类不能直接使⽤, 通过⼀个新类进⾏包装⼀下, 适配调⽤⽅使⽤. 把两个不兼容的接⼝通过⼀定的⽅式使之兼容.
⽐如下⾯两个接⼝, 本⾝是不兼容的(参数类型不⼀样, 参数个数不⼀样等等)
可以通过适配器的⽅式, 使之兼容
⽇常⽣活中, 适配器模式也是⾮常常⻅的
⽐如转换插头, ⽹络转接头等
出国旅⾏必备物品之⼀就是转换插头. 不同国家的插头标准是不⼀样的, 出国后我们⼿机/电脑充电器可能就没办法使⽤了. ⽐如美国电器 110V,中国 220V,就要有⼀个适配器将 110V 转化为 220V. 国内也经常使⽤转换插头把两头转为三头, 或者三头转两头
适配器模式⻆⾊
- Target: ⽬标接⼝ (可以是抽象类或接⼝), 客⼾希望直接⽤的接⼝
- Adaptee: 适配者, 但是与Target不兼容
- Adapter: 适配器类, 此模式的核⼼. 通过继承或者引⽤适配者的对象, 把适配者转为⽬标接⼝
- client: 需要使⽤适配器的对象
适配器模式的实现
场景: 前⾯学习的slf4j 就使⽤了适配器模式, slf4j提供了⼀系列打印⽇志的api, 底层调⽤的是log4j 或者logback来打⽇志, 我们作为调⽤者, 只需要调⽤slf4j的api就⾏了.
/**
* slf4j接⼝
*/
public interface Slf4jApi {
void log(String message);
}
/**
* log4j 接⼝
*/
public class Log4j {
public void log(String message){
System.out.println("Log4j:"+message);
}
}
/**
* slf4j和log4j适配器
*/
public class Slf4jLog4jAdapter implements Slf4jApi{
private Log4j log4j;
public Slf4jLog4jAdapter(Log4j log4j){
this.log4j=log4j;
}
@Override
public void log(String message) {
log4j.log(message);
}
}
/**
* 客⼾端调⽤
*/
public class Main {
public static void main(String[] args) {
Slf4jApi api=new Slf4jLog4jAdapter(new Log4j());
api.log("我是通过Slf4j打印的");
}
}
- Target: ⽬标接⼝,
Slf4jApi
- Adaptee: 适配者,
Log4j
- Adapter: 适配器类,
Slf4jLog4jAdapter
- client: 需要使⽤适配器的对象,
Main
可以看出, 我们不需要改变log4j的api,只需要通过适配器转换下, 就可以更换⽇志框架, 保障系统的平稳运⾏.
适配器模式的实现并不在slf4j-core中(只定义了Logger), 具体实现是在针对log4j的桥接器项⽬slf4jlog4j12中
设计模式的使⽤⾮常灵活, ⼀个项⽬中通常会含有多种设计模式.
适配器模式应⽤场景
⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷. 应⽤这种模式算是"⽆奈之举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新的功能. ⽐如版本升级等.
统⼀数据返回格式
强制登录案例中, 我们共做了两部分⼯作
- 通过Session来判断⽤⼾是否登录
- 对后端返回数据进⾏封装, 告知前端处理的结果
回顾
后端统⼀返回结果
@Data public class Result<T> { //业务状态码 private ResultCode code;//0 成功 -1 失败 -2 未登录 //错误信息 private String errMsg; //数据 private T data; }
后端逻辑处理
@RequestMapping("/getBookListByPage") //为了方便更好拓展,最好返回结果也是一个对象 public Result getBookListByPage(PageRequest pageRequest, HttpSession session) { log.info("查询翻页信息,pageRequest:{}", pageRequest); 用户登录校验 //UserInfo userInfo= (UserInfo) session.getAttribute("session_user_key"); //if(userInfo==null||userInfo.getId()<=0||"".equals(userInfo.getUserName())){ // //用户未登录 // return Result.unLogin(); //} //校验成功 if (pageRequest.getPageSize() < 0 || pageRequest.getCurrentPage() < 1) { //每页显示条数为负或者当前页数不为正数则错误 return Result.fail("参数校验失败"); } PageResult<BookInfo> bookInfoPageResult = null; try { bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest); return Result.success(bookInfoPageResult); } catch (Exception e) { log.error("查询翻页信息错误,e:{}", e); return Result.fail(e.getMessage()); } }
Result.success(pageResult) 就是对返回数据进⾏了封装
拦截器帮我们实现了第⼀个功能, 接下来看SpringBoot对第⼆个功能如何⽀持
快速⼊⻔
统⼀的数据返回格式使⽤ @ControllerAdvice
和 ResponseBodyAdvice
接口 的⽅式实现 @ControllerAdvice
表⽰控制器通知类
添加类 ResponseAdvice
, 实现 ResponseBodyAdvice
接⼝, 并在类上添加 @ControllerAdvice
注解
@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) {
//返回之前,需要做的事情
//body就是返回的结果
return Result.success(body);
}
}
- supports⽅法: 判断是否要执⾏beforeBodyWrite⽅法. true为执⾏, false不执⾏. 通过该⽅法可以选择哪些类或哪些⽅法的response要进⾏处理, 其他的不进⾏处理.
从returnType获取类名和⽅法名
- beforeBodyWrite⽅法: 对response⽅法进⾏具体操作处理
测试
测试接⼝: http://127.0.0.1:8080/book/queryBookInfoById?bookId=1
添加统⼀数据返回格式之前:
添加统⼀数据返回格式之后:
存在问题
问题现象:
我们继续测试修改图书的接⼝: http://127.0.0.1:8080/book/updateBook
结果显⽰, 发⽣内部错误
查看数据库, 发现数据操作成功
查看⽇志, ⽇志报错
多测试⼏种不同的返回结果, 发现只有返回结果为String类型时才有这种错误发⽣.
即请求返回类型是Result时就不需要再进行处理了
返回结果为String时不能正确处理
测试代码:
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("t1")
public Boolean t1(){
return true;
}
@RequestMapping("t2")
public Integer t2(){
return 123;
}
@RequestMapping("t3")
public String t3(){
return "hello";
}
@RequestMapping("t4")
public BookInfo t4(){
return new BookInfo();
}
@RequestMapping("t5")
public Result t5(){
return Result.success("success");
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
//包含一些不应该被拦截的的URL路径
private static List<String> excludePath = Arrays.asList(
"/user/login",//排除这个特定的路径
//因为我们写的不是完全的前后端分离
//下面是为了拦截前端部分的静态资源
"/css/**",
"/js/**",
"/pic/**",
"/**/*.html",
"/test/**");
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)//添加了拦截器
.addPathPatterns("/**")// /**表示给所有方法添加拦截器,即匹配所有路径
.excludePathPatterns(excludePath);
}
}
解决⽅案:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//返回之前,需要做的事情
//body就是返回的结果
if(body instanceof Result){
return body;
}
if(body instanceof String){
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
重新测试, 结果返回正常:
原因分析:
SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter
(从先后顺序排列分别为ByteArrayHttpMessageConverter
,StringHttpMessageConverter
, SourceHttpMessageConverter
,SourceHttpMessageConverter
, AllEncompassingFormHttpMessageConverter
)
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
//...
public RequestMappingHandlerAdapter() {
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
} catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter())
}
//...
}
其中 AllEncompassingFormHttpMessageConverter
会根据项⽬依赖情况 添加对应的HttpMessageConverter
public AllEncompassingFormHttpMessageConverter() {
if(!shouldIgnoreXml){
try {
addPartConverter(new SourceHttpMessageConverter<>());
} catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
}
if(kotlinSerializationJsonPresent){
addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
if(jackson2Present){
addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if(gsonPresent){
addPartConverter(new GsonHttpMessageConverter());
}
else if(jsonbPresent){
addPartConverter(new JsonbHttpMessageConverter());
}
if(jackson2XmlPresent&&!shouldIgnoreXml){
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
if(jackson2SmilePresent){
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
}
在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter
⾃动注册到messageConverters
链的末尾.
Spring会根据返回的数据类型, 从 messageConverters
链选择合适的HttpMessageConverter
.
当返回的数据是⾮字符串时, 使⽤的 MappingJackson2HttpMessageConverter
写⼊返回对象.
当返回的数据是字符申时, StringHttpMessageConverter
会先被遍历到,这时会认为StringHttpMessageConverter
可以使⽤.
public abstract class AbstractMessageConverterMethodProcessor extends
AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
//...代码省略
protected <T> void writeWithMessageConverters(@Nullable T value,
MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse
outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException,
HttpMessageNotWritableException {
//...代码省略
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter
instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter)
converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
//getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的
结果
body = getAdvice().beforeBodyWrite(body, returnType,
selectedMediaType,
20 (Class<? extends HttpMessageConverter<?>>)
converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody,
!traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType,
selectedMediaType, outputMessage);
} else {
//此时cover为StringHttpMessageConverter
((HttpMessageConverter) converter).write(body,
selectedMediaType, outputMessage);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
//...代码省略
}
//...代码省略
}
在 ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage)
的处理中, 调⽤⽗类的write⽅法
由于 StringHttpMessageConverter
重写了addDefaultHeaders⽅法, 所以会执⾏⼦类的⽅法
然⽽⼦类 StringHttpMessageConverter
的addDefaultHeaders⽅法定义接收参数为String, 此时t为Result类型, 所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常
案例代码修改
如果⼀些⽅法返回的结果已经是Result类型了, 那就直接返回Result类型的结果即可
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//返回之前,需要做的事情
//body就是返回的结果
if(body instanceof Result){
return body;
}
if(body instanceof String){
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
@SneakyThrows
是lombok的一个注解,会自动帮我们加上trycatch
的
优点
-
⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据
-
降低前端程序员和后端程序员的沟通成本, 按照某个格式实现就可以了, 因为所有接⼝都是这样返回的.
-
有利于项⽬统⼀数据的维护和修改.
-
有利于后端技术部⻔的统⼀规范的标准制定, 不会出现稀奇古怪的返回内容.
统⼀异常处理
统⼀异常处理使⽤的是 @ControllerAdvice
+ @ExceptionHandler
来实现的,@ControllerAdvice
表⽰控制器通知类, @ExceptionHandler
是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件
具体代码如下:
@Slf4j
@ResponseBody
@ControllerAdvice
public class ErrorHandler {
@ExceptionHandler
public Result exception(Exception e){
log.error("发生异常,e:{}",e);
return Result.fail("内部错误");
}
}
类名, ⽅法名和返回值可以⾃定义, 重要的是注解
接⼝返回为数据时, 需要加
@ResponseBody
注解,如果不加这个注解就认为返回的是页面类上面三个注解都要加,还有方法上的那个注解
以上代码表⽰,如果代码出现 Exception 异常(包括 Exception 的⼦类), 就返回⼀个 Result 的对象, Result 对象的设置参考 Result.fail(e.getMessage())
public static <T>Result<T> fail(String errMsg){
Result result=new Result();
result.setCode(ResultCode.FAIL);
result.setErrMsg(errMsg);
result.setData(null);
return result;
}
我们可以针对不同的异常, 返回不同的结果.
@ResponseBody
@Slf4j
@ControllerAdvice
public class ErrorHandler {
@ExceptionHandler
public Result exception(Exception e){
log.error("发生异常,e:{}",e);
return Result.fail("内部错误");
}
@ExceptionHandler
public Result exception(NullPointerException e){
log.error("发生异常,e:{}",e);
return Result.fail("NullPointerException 异常");
}
@ExceptionHandler
public Result exception(ArithmeticException e){
log.error("发生异常,e:{}",e);
return Result.fail("ArithmeticException 异常");
}
}
模拟制造异常:
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("t1")
public Boolean t1(){
int a=1/0;
return true;
}
@RequestMapping("t2")
public Integer t2(){
String a=null;
System.out.println(a.length());
return 123;
}
@RequestMapping("t3")
public String t3(){
int[] a={1,2,3};
System.out.println(a[5]);
return "hello";
}
}
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
/test/t1
抛出ArithmeticException, 运⾏结果如下:
/test/t2
抛出NullPointerException, 运⾏结果如下:
/test/t3
抛出Exception, 运⾏结果如下:
log.error("发生异常,e:{}",e);
以上代码最好都加上这句,不然比如这里调用/test/t3
就不会在控制台出现这些错误日志了
@ControllerAdvice 源码分析
统⼀数据返回和统⼀异常都是基于 @ControllerAdvice
注解来实现的, 通过分析 @ControllerAdvice
的源码, 可以知道他们的执⾏流程.
点击 @ControllerAdvice 实现源码如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}
从上述源码可以看出 @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);
}
//...
}
对于 @ControllerAdvice
注解,我们重点关注 initHandlerAdapters(context)
和initHandlerExceptionResolvers(context)
这两个⽅法.
- initHandlerAdapters(context)
initHandlerAdapters(context) ⽅法会取得所有实现了 HandlerAdapter
接⼝的bean并保存起来,其中有⼀个类型为 RequestMappingHandlerAdapter
的bean,这个bean就是 @RequestMapping
注解能起作⽤的关键,这个bean在应⽤启动过程中会获取所有被 @ControllerAdvice
注解标注的bean对象, 并做进⼀步处理,关键代码如下:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
//...
/**
* 添加ControllerAdvice bean的处理
*/
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
//获取所有所有被 @ControllerAdvice 注解标注的bean对象
List<ControllerAdviceBean> adviceBeans =
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for
ControllerAdviceBean:" + adviceBean);
}
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType,
MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
Set<Method> binderMethods =
MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(adviceBean, binderMethods);
}
if (RequestBodyAdvice.class.isAssignableFrom(beanType) ||
ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
requestResponseBodyAdviceBeans.add(adviceBean);
}
}
if (!requestResponseBodyAdviceBeans.isEmpty()) {
this.requestResponseBodyAdvice.addAll(0,
requestResponseBodyAdviceBeans);
}
if (logger.isDebugEnabled()) {
int modelSize = this.modelAttributeAdviceCache.size();
int binderSize = this.initBinderAdviceCache.size();
int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);
int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);
if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount
== 0) {
logger.debug("ControllerAdvice beans: none");
} else {
logger.debug("ControllerAdvice beans: " + modelSize + "
@ModelAttribute," + binderSize +
" @InitBinder, " + reqCount + " RequestBodyAdvice, " +
resCount + " ResponseBodyAdvice");
}
}
}
//...
}
这个⽅法在执⾏时会查找使⽤所有的 @ControllerAdvice
类,把 ResponseBodyAdvice
类放在容器中,当发⽣某个事件时,调⽤相应的 Advice ⽅法,⽐如返回数据前调⽤统⼀数据封装⾄于DispatcherServlet和RequestMappingHandlerAdapter是如何交互的这就是另⼀个复杂的话题了,此处不赘述, 源码部分难度⽐较⾼, 且枯燥, ⼤家以了解为主.
- initHandlerExceptionResolvers(context)
接下来看 DispatcherServlet
的 initHandlerExceptionResolvers(context)
⽅法,这个⽅法会取得所有实现了 HandlerExceptionResolver
接⼝的bean并保存起来,其中就有⼀个类型为 ExceptionHandlerExceptionResolver
的bean,这个bean在应⽤启动过程中会获取所有被 @ControllerAdvice
注解标注的bean对象做进⼀步处理, 代码如下:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {
//...
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
// 获取所有所有被 @ControllerAdvice 注解标注的bean对象
List<ControllerAdviceBean> adviceBeans =
ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for
ControllerAdviceBean:" + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new
ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
if (logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
logger.debug("ControllerAdvice beans: none");
} else {
logger.debug("ControllerAdvice beans: " +
handlerSize + " @ExceptionHandler, " + adviceSize + "
ResponseBodyAdvice");
}
}
}
//...
}
当Controller抛出异常时, DispatcherServlet
通过 ExceptionHandlerExceptionResolver
来解析异常,⽽ExceptionHandlerExceptionResolver
⼜通过 ExceptionHandlerMethodResolver
来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适⽤的@ExceptionHandler标注的⽅法是这⾥:
public class ExceptionHandlerMethodResolver {
//...
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList();
//根据异常类型, 查找匹配的异常处理⽅法
//⽐如NullPointerException会匹配两个异常处理⽅法:
//handler(Exception e) 和 handler(NullPointerException e)
for (Class<? extends Throwable> mappedException :
this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
//如果查找到多个匹配, 就进⾏排序, 找到最使⽤的⽅法. 排序的规则依据抛出异常相对于
声明异常的深度
//⽐如抛出的是NullPointerException(继承于RuntimeException,
RuntimeException⼜继承于Exception)
//相对于handler(NullPointerException e) 声明的NullPointerException深度为0,
//相对于handler(Exception e) 声明的Exception 深度 为2
//所以 handler(NullPointerException e)标注的⽅法会排在前⾯
if (!matches.isEmpty()) {
if (matches.size() > 1) {
matches.sort(new ExceptionDepthComparator(exceptionType));
}
return this.mappedMethods.get(matches.get(0));
} else {
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
}
}
//...
}
案例代码
通过上⾯统⼀功能的添加, 我们后端的接⼝已经发⽣了变化(后端返回的数据格式统⼀变成了Result类型), 所以我们需要对前端代码进⾏修改
实际开发中, 后端接⼝的设计需要经过多⽅评审检查(review). 在接⼝设计时就会考虑格式化的统⼀化,尽可能的避免返⼯
当前是学习阶段, 给⼤家讲了这个接⼝设计的演变过程
登录⻚⾯
登录界⾯没有拦截, 只是返回结果发⽣了变化, 所以只需要根据返回结果修改对应代码即可
登录结果代码修改
function login() {
$.ajax({
url:"/user/login",
type:"post",
data:{
"userName":$("#userName").val(),
"password":$("#password").val()
},
success:function(result){
console.log(result);
if(result!=null&&result.code=="SUCCESS"&&result.data==true){
location.href = "book_list.html";
}else{
alert("用户名或密码错误");
}
}
});
}
图书列表
针对图书列表⻚有两处变化
- 拦截器进⾏了强制登录校验, 如果校验失败, 则http状态码返回401, 此时会⾛ajax的error逻辑处理
- 接⼝返回结果发⽣了变化
图书列表代码修改:
function getBookList() {
$.ajax({
type: "get",
url: "/book/getBookListByPage" + location.search,
success: function (result) {
//真实的前端处理逻辑比后端复杂
if (result.code == "UNLOGIN") {
location.href = "login.html";
return;
}
var finalHtml = "";
//加载列表
var pageResult = result.data;
for (var book of pageResult.records) {
//根据每一条记录拼接html,也就是一个<tr>
finalHtml += '<tr>';
finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';
finalHtml += '<td>' + book.id + '</td>';
finalHtml += '<td>' + book.bookName + '</td>';
finalHtml += '<td>' + book.author + '</td>';
finalHtml += '<td>' + book.count + '</td>';
finalHtml += '<td>' + book.price + '</td>';
finalHtml += '<td>' + book.publish + '</td>';
finalHtml += '<td>' + book.statusCN + '</td>';
finalHtml += '<td>';
finalHtml += '<div class="op">';
finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
finalHtml += '<a href="javascript:void(0)" οnclick="deleteBook(' + book.id + ')">删除</a>';
finalHtml += '</div>';
finalHtml += '</td>';
finalHtml += '</tr>';
}
$("tBody").html(finalHtml);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: pageResult.total, //总记录数
pageSize: 10, //每页的个数
visiblePages: 5, //可视页数
currentPage: pageResult.pageRequest.currentPage, //当前页码
first: '<li class="page-item"><a class="page-link">首页</a></li>',
prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
//页面初始化和页码点击时都会执行
onPageChange: function (page, type) {
console.log("第" + page + "页, 类型:" + type);
if (type == "change") {
location.href = "book_list.html?currentPage=" + page;
}
}
});
},
error: function (error) {
console.log(error);
if (error.status == 401) {
console.log("401");
location.href = "login.html";
}
}
});
}
其他
参考图书列表, 对删除图书, 批量删除图书,添加图书, 修改图书接⼝添加⽤⼾强制登录以及统⼀格式返回的逻辑处理
- 删除图书
function deleteBook(bookId) {
var isDelete = confirm("确认删除?");
if (isDelete) {
//删除图书
$.ajax({
type: "post",
url: "/book/updateBook",
data: {
id: bookId,
status: 0
},
success: function (result) {
if (result != null && result.code == "SUCCESS" && result.data == "") {
//删除成功
location.href = "book_list.html";
} else {
alert(result);
}
},
error: function (error) {
console.log(error);
//用户未登录
if (error != null && error.status == 401) {
location.href = "login.html";
}
}
});
}
}
- 批量删除图书
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
//获取复选框的id
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
console.log(ids);
$.ajax({
type: "post",
url: "/book/batchDelete?ids=" + ids,
success: function (result) {
if (result != null && result.code == "SUCCESS" && result.data == "") {
//删除成功
location.href = "book_list.html";
} else {
alert(result);
}
},
error: function (error) {
console.log(error);
//用户未登录
if (error != null && error.status == 401) {
location.href = "login.html";
}
}
});
}
}
- 添加图书
如果后端返回的结果是String类型,当我们用统一结果返回时,返回的是JSON字符串,content-type 是 text/html,我们需要把它转为JSON
如果后端进行转换:
@RequestMapping(value = "/addBook",produces = "application/json") public String addBook(BookInfo bookInfo)
如果前端进行转换:把字符串转为对象
JSON.parse(result)
function add() {
$.ajax({
type: "post",
url: "/book/addBook",
data: $("#addBook").serialize(),//提交整个form表单
success: function (result) {
console.log(result);
console.log(typeof result)
if (result != null && result.code == "SUCCESS" && result.data == "") {
//图书添加成功
location.href = "book_list.html";
} else {
alert(result);
}
},
error: function (error) {
console.log(error);
//用户未登录
if (error != null && error.status == 401) {
location.href = "login.html";
}
}
});
}
- 获取图书详情
$.ajax({
type: "get",
url: "/book/queryBookInfoById" + location.search,
success: function (result) {
if (result != null && result.code == "SUCCESS") {
var book = result.data;
if (book != null) {
//页面输入框的填充
$("#bookId").val(book.id);
$("#bookName").val(book.bookName);
$("#bookAuthor").val(book.author);
$("#bookStock").val(book.count);
$("#bookPrice").val(book.price);
$("#bookPublisher").val(book.publish);
$("#bookStatus").val(book.status);
} else {
alert("图书不存在");
}
} else {
alert(result.errMsg);
}
},
error: function (error) {
console.log(error);
//用户未登录
if (error != null && error.status == 401) {
location.href = "login.html";
}
}
});
- 修改图书
function update() {
$.ajax({
type: "post",
url: "/book/updateBook",
data: $("#updateBook").serialize(),
success: function (result) {
if (result != null && result.code == "SUCCESS" && result.data == "") {
location.href = "book_list.html";
} else {
alert(result);
}
},
error: function (error) {
console.log(error);
//用户未登录
if (error != null && error.status == 401) {
location.href = "login.html";
}
}
});
}
总结
本章节主要介绍了SpringBoot 对⼀些统⼀功能的处理⽀持.
- 拦截器的实现主要分两部分: 1. 定义拦截器(实现HandlerInterceptor 接⼝) 2. 配置拦截器
- 统⼀数据返回格式通过@ControllerAdvice + ResponseBodyAdvice 来实现
- 统⼀异常处理使⽤@ControllerAdvice + @ExceptionHandler 来实现, 并且可以分异常来处理
- 学习了DispatcherServlet的⼀些源码.