视图控制器
视图控制器,也就是我们所说的viewController,不用通过在Controller中增加映射方法的情况下,通过简单的设置就可以把前端指定的请求映射到相应的页面。接下来我们还是以上一个employee-management项目为例,可以看到我们登入首页(login.html)和主面板页(dashboard.html)的请求都在Controller中有对应的方法
@GetMapping("/login") public String login(){ return "login"; } @GetMapping("/dashboard") public String dashboard(){ return "dashboard"; }
这样一来当请求过多时,就会有大量的与业务无关的代码被写在Controller中,因此来借助SpringBoot的ViewController来代替Controller中这些代码
首先在项目下新建一个config的包,在包下创建一个自己的WebMvcConfig配置类,该类要实现WebMvcConfigurer接口,代码如下
@Configurationpublic class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("/login"); registry.addViewController("/login.html").setViewName("/login"); registry.addViewController("/dashboard.html").setViewName("/dashboard"); }}
然后把我们LoginController中的login和dashboard方法注释掉,可以看到我们无论是通过"localhost:8080/"还是"localhost:8080/login.html"都可以访问我们登入页。
同时输入用户名、密码(123456)也可以正常访问主面板页和员工列表页,到这里我们就实现了通过ViewController等价替换Controller中编写方法实现请求到视图映射的功能。
同时我们也看到WebMvcConfigurer还有一些其他方法,如下
// 格式化器 default void addFormatters(FormatterRegistry registry) {} // 拦截器 default void addInterceptors(InterceptorRegistry registry) {} // 视图解析器 default void configureViewResolvers(ViewResolverRegistry registry) {}
登入成功就来到了主面板页(http://localhost:8080/dashboard.html),此时我们把链接放入其他浏览器,发现也可以正常访问,这就失去了我们登入功能的意义,接下来将通过注册拦截器来实现登入后才能访问的功能。
拦截器
接下来我们将通过WebMvcConfigurer的addInterceptors()方法向项目中注册拦截器。
首先定义自己的拦截器类LoginHandlerInterceptor实现HandlerInterceptor接口,HandlerInterceptor接口有3个方法,如下
public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {} default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}}
接下来我们重写preHandle方法,来实现请求的预处理,大致思路是先从session中拿登入用户信息,如果能拿到说明以登入可以继续访问,否则跳转登入页,并携带提示用户没有权限等登入的信息,代码如下
public class LoginHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if(loginUser == null){ request.setAttribute("msg","没有登入权限"); request.getRequestDispatcher("/login.html").forward(request,response); return false; } return true; }}
这样我们自己的拦截器类就开发完成了,接下来我们把这个拦截器添加到项目中,在WebMvcConfig类中重写addInterceptors方法,如下
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginHandlerInterceptor()).excludePathPatterns("/login","/login.html","/user/login","/assets/**"); }
同时拦截器需要放过我们登入页、登入请求、静态资源访问的请求,我们用excludePathPatterns方法中添加想要放行的请求,否则我们将不能访问任何页面了。接下来我们看一下效果,在未登入状态下访问我们主面板页,直接跳转到了登入页并且提示“没有登入权限”,一切正常。
SpringBoot对WebMvcConfig的加载过程
首先在我们的addInterceptors和addViewControllers两个方法上分别打上断点,然后debug我们这个项目,如下:
依次在上图上标识出了三个部分:断点位置、变量区、调试过程区,接下来我们重点看一下调试过程和变量区,关键过程如下:
首先是从主入口的run方法进来
SpringApplication.run(EmployeeManagementApplication.class, args);
接下来就到了我们的刷新上下文
public void refresh() throws BeansException, IllegalStateException { synchronized(this.startupShutdownMonitor) { // 准备刷新 this.prepareRefresh(); ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory(); // 准备BeanFactory以便在上下文中使用 this.prepareBeanFactory(beanFactory); try { this.postProcessBeanFactory(beanFactory); this.invokeBeanFactoryPostProcessors(beanFactory); this.registerBeanPostProcessors(beanFactory); this.initMessageSource(); this.initApplicationEventMulticaster(); this.onRefresh(); this.registerListeners(); // 完成BeanFactory的初始化,加载项目中非懒加载的Bean,单例模式 this.finishBeanFactoryInitialization(beanFactory); this.finishRefresh(); } catch (BeansException var9) { if (this.logger.isWarnEnabled()) { this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9); } this.destroyBeans(); this.cancelRefresh(var9); throw var9; } finally { this.resetCommonCaches(); } }}
在BeanFactory初始化的过程中,从beanDefinitionNames拿到了需要初始化的beanNames,其中有一个名称为requestMappingHandlerMapping的bean,如下
之后通过代理模式在WebMvcAutoConfiguration.class中找到了bean注册的位置
@Bean@Primarypublic RequestMappingHandlerMapping requestMappingHandlerMapping(@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { return super.requestMappingHandlerMapping(contentNegotiationManager, conversionService, resourceUrlProvider);}// 接下来我们看父类中的requestMappingHandlerMapping方法mapping.setInterceptors(this.getInterceptors(conversionService, resourceUrlProvider));// 这个getInterceptors具体代码如下protected final Object[] getInterceptors(FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { if (this.interceptors == null) { InterceptorRegistry registry = new InterceptorRegistry(); // 这个方法遍历所有的delegates调用他们的addInterceptors方法注册拦截器 this.addInterceptors(registry); // 在这里补充一下addInterceptors方法 // public void addInterceptors(InterceptorRegistry registry) { // Iterator var2 = this.delegates.iterator(); // while(var2.hasNext()) { // WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next(); // delegate.addInterceptors(registry); // } // } registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService)); registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider)); // 获取registry中的拦截器,并存入interceptors数组中,这个在请求前置拦截时会用到 this.interceptors = registry.getInterceptors(); } return this.interceptors.toArray();}
其中delegates数组中就有我们自己定义的WebMvcConfig类
@Overridepublic void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginHandlerInterceptor()).excludePathPatterns("/login","/login.html","/user/login","/assets/**");}
那拦截器是怎么生效的呢,项目完全启动起来,我们直接访问我们的员工列表页面(http://localhost:8080/emps),看一下请求路径是怎么样的,首先我们从DispatchServlet类的doDispatch方法开始看起,先看一张大图辅助理解
我们本次分析主要是HandlerInterceptor部分,其他的后续有涉及咱们会再依次介绍。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... ... try { ... // 通过handlerMapping获取HandlerExecutionChain,其中就有RequestMappingHandlerMapping mappedHandler = this.getHandler(processedRequest); // 把getHanlder方法放在这里方便大家阅读 @Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { // 其中handlerMappings中的RequestMappingHandlerMapping的interceptors中就有我们的LoginHandlerInterceptor,因此在后续中就会调用LoginHandlerInteceptor的preHandle方法 for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; } ... ... // 调用获取到的mapperHandler的PreHandle方法,调用时机是controller处理之前 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 真正去调用处理程序,我们注册的viewController等都会在这里被生效. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... ... this.applyDefaultViewName(processedRequest, mv); // 调用获取到的mapperHandler的PostHandle方法,调用时机是controller处理之后 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } ...}
因此也就能够顺利的向项目中注册我们自己定义的拦截器了。
同样在BeanFactory初始化的过程中,拿到了beanName为viewControllerHandlerMapping,注册bean的代码如下
@Bean@Nullablepublic HandlerMapping viewControllerHandlerMapping(@Qualifier("mvcPathMatcher") PathMatcher pathMatcher, @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) { ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext); this.addViewControllers(registry); AbstractHandlerMapping handlerMapping = registry.buildHandlerMapping(); if (handlerMapping == null) { return null; } else { handlerMapping.setPathMatcher(pathMatcher); handlerMapping.setUrlPathHelper(urlPathHelper); handlerMapping.setInterceptors(this.getInterceptors(conversionService, resourceUrlProvider)); handlerMapping.setCorsConfigurations(this.getCorsConfigurations()); return handlerMapping; }}
同样是遍历delegates调用各delegate的addViewControllers方法,同时能够顺利的向项目中注册我们自己定义的视图控制器了,同时通过getHandler获取处理请求的Handler,handler中包含请求映射的视图的名称,也就是我们设置的setViewName中的名称,这样就得到了请求到视图的映射。
@Overridepublic void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("/login"); registry.addViewController("/login.html").setViewName("/login"); registry.addViewController("/dashboard.html").setViewName("/dashboard");}
请求的详细处理过程,我们后续再给大家介绍。