网站制作过程流程,网站建设方案图,自己做网站怎么样,网站里面嵌入的地图是怎么做的目录 SpringBoot 统⼀功能处理拦截器拦截器快速⼊⻔拦截器详解拦截路径拦截器执⾏流程 登录校验定义拦截器注册配置拦截器 DispatcherServlet 源码分析(了解)初始化(了解) DispatcherServlet的初始化1. HttpServletBean.init()2. FrameworkServlet.initServletBean() WebApplic… 目录 SpringBoot 统⼀功能处理拦截器拦截器快速⼊⻔拦截器详解拦截路径拦截器执⾏流程 登录校验定义拦截器注册配置拦截器 DispatcherServlet 源码分析(了解)初始化(了解) DispatcherServlet的初始化1. HttpServletBean.init()2. FrameworkServlet.initServletBean() WebApplicationContext的建立和配置1. FrameworkServlet.initWebApplicationContext() 初始化Spring MVC的9大组件1. FrameworkServlet.onRefresh()2. DispatcherServlet.initStrategies() 总结处理请求(核⼼) DispatcherServlet.doDispatch() 方法的流程拦截器HandlerInterceptor的作用总结适配器模式 统⼀数据返回格式快速⼊⻔存在问题案例代码修改优点 统⼀异常处理ControllerAdvice 源码分析案例代码登录⻚⾯图书列表其他 总结 SpringBoot 统⼀功能处理
掌握拦截器的使⽤, 及其原理学习统⼀数据返回格式和统⼀异常处理的操作了解⼀些Spring的源码
拦截器
之前我们完成了强制登录的功能, 后端程序根据Session来判断⽤⼾是否登录, 但是实现⽅法是⽐较⿇烦的
需要修改每个接⼝的处理逻辑需要修改每个接⼝的返回结果接⼝定义修改, 前端代码也需要跟着修改
有没有更简单的办法, 统⼀拦截所有的请求, 并进⾏Session校验呢, 这⾥⼀种新的解决办法: 拦截器
拦截器快速⼊⻔
什么是拦截器?
拦截器是Spring框架提供的核⼼功能之⼀, 主要⽤来拦截⽤⼾的请求, 在指定⽅法前后, 根据业务需要执⾏预先设定的代码.
拦截器的作用维度URL 也就是说, 允许开发⼈员提前预定义⼀些逻辑, 在⽤⼾的请求响应前后执⾏. 也可以在⽤⼾请求前阻⽌其执⾏. 在拦截器当中开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求, 判断Session中是否有登录⽤⼾的信息. 如果有就可以放⾏, 如果没有就进⾏拦截. ⽐如我们去银⾏办理业务, 在办理业务前后, 就可以加⼀些拦截操作 办理业务之前, 先取号, 如果带⾝份证了就取号成功 业务办理结束, 给业务办理⼈员的服务进⾏评价. 这些就是拦截器做的⼯作 拦截器的基本使⽤
拦截器的使⽤步骤分为两步
定义拦截器注册配置拦截器
⾃定义拦截器实现HandlerInterceptor接⼝并重写其所有⽅法
public class LoginInterceptor implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {return HandlerInterceptor.super.preHandle(request, response, handler);}Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}Overridepublic 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 {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info(目标方法执行前);return true;}Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info(目标方法执行后);}
}注册配置拦截器实现WebMvcConfigurer接⼝并重写addInterceptors⽅法
Configuration
public class WebConfig implements WebMvcConfigurer {Autowiredprivate LoginInterceptor loginInterceptor;Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns(/**);// /**表示给所有方法添加拦截器}
}启动服务, 试试访问任意请求, 观察后端⽇志 可以看到 preHandle ⽅法执⾏之后就放⾏了, 开始执⾏⽬标⽅法, ⽬标⽅法执⾏完成之后执⾏postHandle和afterCompletion⽅法.
我们把拦截器中preHandle⽅法的返回值改为false, 再观察运⾏结果 可以看到, 拦截器拦截了请求, 没有进⾏响应
拦截器详解
拦截器的⼊⻔程序完成之后接下来我们来介绍拦截器的使⽤细节。拦截器的使⽤细节我们主要介绍两个部分
拦截器的拦截路径配置拦截器实现原理
拦截路径
拦截路径是指我们定义的这个拦截器, 对哪些请求⽣效.
我们在注册配置拦截器的时候, 通过 addPathPatterns() ⽅法指定要拦截哪些请求. 也可以通过excludePathPatterns() 指定不拦截哪些请求. 上述代码中, 我们配置的是 /** , 表⽰拦截所有的请求. ⽐如⽤⼾登录校验, 我们希望可以对除了登录之外所有的路径⽣效.
Configuration
public class WebConfig implements WebMvcConfigurer {Autowiredprivate LoginInterceptor loginInterceptor;Overridepublic 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 {Overridepublic 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 {Autowiredprivate LoginInterceptor loginInterceptor;//包含一些不应该被拦截的的URL路径private static ListString excludePath Arrays.asList(/user/login,//排除这个特定的路径//因为我们写的不是完全的前后端分离//下面是为了拦截前端部分的静态资源/css/**,/js/**,/pic/**,/**/*.html);Overridepublic 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(userInfonull||userInfo.getId()0||.equals(userInfo.getUserName())){// //用户未登录// return Result.unLogin();//}//校验成功if (pageRequest.getPageSize() 0 || pageRequest.getCurrentPage() 1) {//每页显示条数为负或者当前页数不为正数则错误return Result.fail(参数校验失败);}PageResultBookInfo 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?nameadminpasswordadmin 再次查看图书列表
数据进⾏了返回 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来配置主要⽤来配置ListenerFilterServlet等, Spring框架从3.1版本开始⽀持Servlet3.0, 并且从3.2版本开始通过配置DispatcherServlet, 实现不再使⽤web.xml init() 具体代码如下 public final void init() throws ServletException {// ServletConfigPropertyValues 是静态内部类使⽤ ServletConfig 获取 web.xmlPropertyValues pvs new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);if (!pvs.isEmpty()) {try {// 使⽤ BeanWrapper 来构造 DispatcherServletBeanWrapper 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 MVCsynchronized(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 找到与请求对应的HandlermappedHandler this.getHandler(processedRequest);if (mappedHandler null) {this.noHandlerFound(processedRequest, response);return;}//2. 获取适配器//遍历所有的 HandlerAdapter找到可以处理该 Handler 的 HandlerAdapterHandlerAdapter 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) {// 获取项⽬中使⽤的拦截器 HandlerInterceptorHandlerInterceptor 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.log4jlog4j;}Overridepublic void log(String message) {log4j.log(message);}
}/*** 客⼾端调⽤*/
public class Main {public static void main(String[] args) {Slf4jApi apinew Slf4jLog4jAdapter(new Log4j());api.log(我是通过Slf4j打印的);}
}Target: ⽬标接⼝Slf4jApiAdaptee: 适配者Log4jAdapter: 适配器类Slf4jLog4jAdapterclient: 需要使⽤适配器的对象Main 可以看出, 我们不需要改变log4j的api,只需要通过适配器转换下, 就可以更换⽇志框架, 保障系统的平稳运⾏. 适配器模式的实现并不在slf4j-core中(只定义了Logger), 具体实现是在针对log4j的桥接器项⽬slf4jlog4j12中 设计模式的使⽤⾮常灵活, ⼀个项⽬中通常会含有多种设计模式. 适配器模式应⽤场景
⼀般来说适配器模式可以看作⼀种补偿模式⽤来补救设计上的缺陷. 应⽤这种模式算是⽆奈之举, 如果在设计初期我们就能协调规避接⼝不兼容的问题, 就不需要使⽤适配器模式了所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造, 并且希望可以复⽤原有代码实现新的功能. ⽐如版本升级等.
统⼀数据返回格式
强制登录案例中, 我们共做了两部分⼯作
通过Session来判断⽤⼾是否登录对后端返回数据进⾏封装, 告知前端处理的结果 回顾 后端统⼀返回结果 Data
public class ResultT {//业务状态码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(userInfonull||userInfo.getId()0||.equals(userInfo.getUserName())){// //用户未登录// return Result.unLogin();//}//校验成功if (pageRequest.getPageSize() 0 || pageRequest.getCurrentPage() 1) {//每页显示条数为负或者当前页数不为正数则错误return Result.fail(参数校验失败);}PageResultBookInfo 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 {Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}Overridepublic 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?bookId1
添加统⼀数据返回格式之前: 添加统⼀数据返回格式之后: 存在问题
问题现象:
我们继续测试修改图书的接⼝: 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 {Autowiredprivate LoginInterceptor loginInterceptor;//包含一些不应该被拦截的的URL路径private static ListString excludePath Arrays.asList(/user/login,//排除这个特定的路径//因为我们写的不是完全的前后端分离//下面是为了拦截前端部分的静态资源/css/**,/js/**,/pic/**,/**/*.html,/test/**);Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor)//添加了拦截器.addPathPatterns(/**)// /**表示给所有方法添加拦截器即匹配所有路径.excludePathPatterns(excludePath);}
}解决⽅案:
ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {Autowiredprivate ObjectMapper objectMapper;Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}SneakyThrowsOverridepublic 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 AbstractHandlerMethodAdapterimplements 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 extendsAbstractMessageConverterMethodArgumentResolverimplements HandlerMethodReturnValueHandler {//...代码省略protected T void writeWithMessageConverters(Nullable T value,MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponseoutputMessage)throws IOException, HttpMediaTypeNotAcceptableException,HttpMessageNotWritableException {//...代码省略if (selectedMediaType ! null) {selectedMediaType selectedMediaType.removeQualityValue();for (HttpMessageConverter? converter : this.messageConverters) {GenericHttpMessageConverter genericConverter (converterinstanceof 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 {ExceptionHandlerpublic Result exception(Exception e){log.error(发生异常,e:{},e);return Result.fail(内部错误);}
}类名, ⽅法名和返回值可以⾃定义, 重要的是注解 接⼝返回为数据时, 需要加 ResponseBody 注解如果不加这个注解就认为返回的是页面 类上面三个注解都要加还有方法上的那个注解 以上代码表⽰如果代码出现 Exception 异常(包括 Exception 的⼦类), 就返回⼀个 Result 的对象, Result 对象的设置参考 Result.fail(e.getMessage())
public static TResultT fail(String errMsg){Result resultnew Result();result.setCode(ResultCode.FAIL);result.setErrMsg(errMsg);result.setData(null);return result;
}我们可以针对不同的异常, 返回不同的结果.
ResponseBody
Slf4j
ControllerAdvice
public class ErrorHandler {ExceptionHandlerpublic Result exception(Exception e){log.error(发生异常,e:{},e);return Result.fail(内部错误);}ExceptionHandlerpublic Result exception(NullPointerException e){log.error(发生异常,e:{},e);return Result.fail(NullPointerException 异常);}ExceptionHandlerpublic Result exception(ArithmeticException e){log.error(发生异常,e:{},e);return Result.fail(ArithmeticException 异常);}
}模拟制造异常:
RequestMapping(/test)
RestController
public class TestController {RequestMapping(t1)public Boolean t1(){int a1/0;return true;}RequestMapping(t2)public Integer t2(){String anull;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 {//...Overrideprotected void onRefresh(ApplicationContext context) {initStrategies(context);}/*** Initialize the strategy objects that this servlet uses.* pMay 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对象ListControllerAdviceBean adviceBeans ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());ListObject requestResponseBodyAdviceBeans new ArrayList();for (ControllerAdviceBean adviceBean : adviceBeans) {Class? beanType adviceBean.getBeanType();if (beanType null) {throw new IllegalStateException(Unresolvable type forControllerAdviceBean: adviceBean);}SetMethod attrMethods MethodIntrospector.selectMethods(beanType,MODEL_ATTRIBUTE_METHODS);if (!attrMethods.isEmpty()) {this.modelAttributeAdviceCache.put(adviceBean, attrMethods);}SetMethod 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对象ListControllerAdviceBean 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 newExceptionHandlerMethodResolver(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) {ListClass? 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!nullresult.codeSUCCESSresult.datatrue){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也就是一个trfinalHtml tr;finalHtml tdinput typecheckbox nameselectBook value book.id idselectBook classbook-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 classop;finalHtml a hrefbook_update.html?bookId book.id 修改/a;finalHtml a hrefjavascript:void(0) οnclickdeleteBook( 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 classpage-itema classpage-link首页/a/li,prev: li classpage-itema classpage-link hrefjavascript:void(0);上一页\/a\/li,next: li classpage-itema classpage-link hrefjavascript:void(0);下一页\/a\/li,last: li classpage-itema classpage-link hrefjavascript:void(0);最后一页\/a\/li,page: li classpage-itema classpage-link hrefjavascript: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) {//获取复选框的idvar ids [];$(input:checkbox[nameselectBook]: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的⼀些源码.