likes
comments
collection
share

Spring Boot「37」用 Thymeleaf 渲染你的界面

作者站长头像
站长
· 阅读数 17

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 22 天,点击查看活动详情

在之前实现 SSO 登录流程时,调试都是通过 Postman 发送请求给后台服务。 登录也是通过直接发送 POST 请求实现的。 虽然能够进行调试,但体验感并不太好。 今天我就来分享下,我是如何给他们增加界面的。

01-请求流程分析

在开始具体细节之前,我先带着大家看一下 Spring Boot Web 应用处理一个请求的流程是怎样的? 视图渲染又是在这个流程的那一步?当我们对整个流程有了了解后,详细对后面如何配置 ViewResolver 以及为什么要这样配置都能有非常清晰的认识。

在去年的一篇文章中层介绍过 Spring MVC 中的 DispatcherServlet。 它使用的设计模式是所谓的前端控制器模式,即 DispatcherServlet 处理所有的请求消息,然后再将他们代理给某个特定的业务控制器去处理。 关于前端控制器模式的介绍可以参考这篇文章 [1]

Spring 官网 对 DispatcherServlet 的处理流程有着详细的介绍。

The DispatcherServlet processes requests as follows:

  1. The WebApplicationContext is searched for and bound in the request as an attribute that the controller and other elements in the process can use. It is bound by default under the DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE key.
  2. The locale resolver is bound to the request to let elements in the process resolve the locale to use when processing the request (rendering the view, preparing data, and so on). If you do not need locale resolving, you do not need the locale resolver.
  3. The theme resolver is bound to the request to let elements such as views determine which theme to use. If you do not use themes, you can ignore it.
  4. If you specify a multipart file resolver, the request is inspected for multiparts. If multiparts are found, the request is wrapped in a MultipartHttpServletRequest for further processing by other elements in the process. See Multipart Resolver for further information about multipart handling.
  5. An appropriate handler is searched for. If a handler is found, the execution chain associated with the handler (preprocessors, postprocessors, and controllers) is run to prepare a model for rendering. Alternatively, for annotated controllers, the response can be rendered (within the HandlerAdapter) instead of returning a view.
  6. If a model is returned, the view is rendered. If no model is returned (maybe due to a preprocessor or postprocessor intercepting the request, perhaps for security reasons), no view is rendered, because the request could already have been fulfilled.

这里面涉及了多个不同的部分,下面我结合者 DispatcherServlet 的源码,和大家一块再复习下这个流程。

首先,上面的流程实现在 org.springframework.web.servlet.DispatcherServlet#doServiceorg.springframework.web.servlet.DispatcherServlet#doDispatch中。

步骤1~3,是讲 DispatcherServlet 会向每个请求中设置三个属性,在后续对请求的处理过程中使用。

// doService
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());    // web 应用使用得容器上下文,后续处理请求时,可以通过 getAttribute 获取到
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);               // locale 解析器
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);                 // theme 解析器
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

步骤4,检查请求是否是 multipart 请求,主要用在大文件上传、下载请求中。

// doDispatch
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

//省略了其他的部分
        
if (multipartRequestParsed) {
    cleanupMultipart(processedRequest);
}

步骤5,查找处理请求的 Handler,并处理请求。

// 查找能够处理请求的方法
mappedHandler = getHandler(processedRequest);

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 调用拦截器(步骤6中的 preprocessor)
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}
// 最终会调用到业务层的处理方法,例如 Controller 中的业务方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 调用拦截器(步骤中的 postprocessor)
mappedHandler.applyPostHandle(processedRequest, response, mv);

步骤6,如果需要渲染视图,则根据 model 渲染视图。

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

从上面的流程中可以知道,我们要通过 Thymeleaf 渲染视图,那就需要修改步骤6。

02-Spring MVC 中的视图解析器

Spring MVC 中定义了 View 和 ViewResolver 接口来抽象视图及其渲染过程,从而屏蔽底层具体使用的模板引擎,例如 JSP、Thymeleaf 等等。 View 是最终得到的结果,它定义了一个关键方法 render,渲染最终的信息到 response 中:

void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;

ViewResolver 接口是用来从 view name(往往是个字符串)解析出合适的 View 对象出来。所以,它只定义了一个核心方法:

View resolveViewName(String viewName, Locale locale) throws Exception;

ViewResolver 的类结构图如下所示: Spring Boot「37」用 Thymeleaf 渲染你的界面

我们重点看一下 ContentNegotiatingViewResolver 和 AbstractCachingViewResolver(它是 ThymeleafViewResolver 的父类)。

02.1-ContentNegotiatingViewResolver

ContentNegotiatingViewResolver 是 Spring 提供的一种 ViewResolver 实现,它会根据请求的文件名或请求头中的 Accept 字段来解析视图。 它对 resolveViewName 的基本实现如下:

public View resolveViewName(String viewName, Locale locale) throws Exception {
    // 从请求中获取客户端支持的 媒体类型
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    
    if (requestedMediaTypes != null) {
        // 根据媒体类型选择候选视图
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        // 从候选视图中选择最优的
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }
}

从上面可以看出,它会尝试生成多个候选视图,并从这些视图中选择最优的返回。 更进一步地去分析发现,ContentNegotiatingViewResolver 包含了一个 ViewResolver 列表,而且类本身继承了 WebApplicationObjectSupport,能在 ServletContext 初始化时被通知到(通过 WebApplicationObjectSupport#initServletContext)。 然后,从应用容器上下文中获取到所有的 ViewResolver 实例,将其保存到自身的 ViewResolver 列表中。 可以通过 Debug 调试来验证这一点。 Spring Boot「37」用 Thymeleaf 渲染你的界面

getCandidateViews 会遍历 ViewResolver 列表,得到所有的候选 View 对象。 getBestView 会根据策略返回最优选项:

  • 如果是重定向视图,则优先返回。
  • 遍历媒体类型,根据媒体类型优先级选择最优视图。

02.2-ThymeleafViewResolver

public View resolveViewName(String viewName, Locale locale) throws Exception {
    // 根据是否使用缓存
    if (!isCache()) {
        // 不是用缓存
        return createView(viewName, locale);
    }
    else{
        // 使用缓存
        // 省略这部分,缓存是一种优化手段,暂时先忽略
    }
}
protected View createView(String viewName, Locale locale) throws Exception {
    return loadView(viewName, locale);  
}
@Nullable
protected abstract View loadView(String viewName, Locale locale) throws Exception;

ThymeleafViewResolver 作为 AbstractCachingViewResolver 的子类,主要看它对 createView 和 loadView 的实现。 注:这部分代码在 thymeleaf-spring5-*.jar 包中。

02.3-ThymeleafView

经过解析器的解析,我们最终会得到的一个 ThymeleafView。 前面我也提到,View 接口只定义了一个方法,即 render 方法。它会将渲染后的内容,写入到 response 中。 我们一起看下 ThymeleafView#render 的实现。 注:Thymeleaf 是一个模板引擎,ThymeleafView#render 最终会交给模板引擎去处理。下面的代码我会省略很多的细节,只展示部分关键流程代码。

// 这里取到的就是 Thymeleaf 中的引擎实例, ISpringTemplateEngine 是对 ITemplateEngine 接口的扩展
final ISpringTemplateEngine viewTemplateEngine = getTemplateEngine();

// 是模板引擎填充模板需要的上下文对象
final WebExpressionContext context =
        new WebExpressionContext(configuration, webExchange, getLocale(), mergedModel);

// 这里的 templateWriter,来自于 response,或最终会写入到 response 中
viewTemplateEngine.process(templateName, processMarkupSelectors, context, templateWriter);

扩展: 在 Thymeleaf 中,通过模板 + context 能够生成具体的 html 文件,这个过程称为模板引擎执行一次模板。 执行一次模板需要两个元素: 模板执行(template execution),需要的对象 IContext,包括两部分:

  1. locale
  2. context variables。Thymeleaf 提供了两个 Context 实现:Context 和 WebContext。

最终,模板渲染的请求交友 viewTemplateEngine#process 方法处理。

02.4-Spring Boot 对 Thymeleaf 的自动化配置

这里我是用的 Spring Boot 版本是 2.7.6。 在 spring-boot-autoconfigure 包中对 Thymeleaf 的配置集中在 org.springframework.boot.autoconfigure.thymeleaf 中。 包里面包括四个类:

  • TemplateEngineConfigurations,对 Thymeleaf 模板引擎的配置
  • ThymeleafAutoConfiguration,Spring Boot 的自动化配置类
  • ThymeleafProperties,与 Thymeleaf 相关的配置信息,可以在 application.yml 中通过参数来配置 Thymeleaf 的行为
  • ThymeleafTemplateAvailabilityProvider,只提供了一个方法,用来检查模板文件是否存在
@Bean
@ConditionalOnMissingBean(ISpringTemplateEngine.class)
SpringTemplateEngine templateEngine(ThymeleafProperties properties,
        ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {  // 通过这些参数知道,我们可以向 Spring 应用上下文容器中添加类来影响模板引擎
    SpringTemplateEngine engine = new SpringTemplateEngine();   // 创建模板引擎类实例
    engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
    engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
    templateResolvers.orderedStream().forEach(engine::addTemplateResolver);  // 设置 template resolver
    dialects.orderedStream().forEach(engine::addDialect);
    return engine;
}

注:TemplateResolver 是模板引擎使用的解析器,用来根据名字找到对应的模板文件。例如,根据 “home” 查找在 classpath:/WEB-INF/templates/home.html 的模板文件。 所以,如果我们要自定义模板查找位置,可以通过修改模板引擎使用的模板解析器来实现。

Spring Boot 实例化了一个 SpringResourceTemplateResolver 作为默认的模板解析器。

@Bean
SpringResourceTemplateResolver defaultTemplateResolver() {
    SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
    resolver.setApplicationContext(this.applicationContext);
    resolver.setPrefix(this.properties.getPrefix());      // 这里默认使用 classpath:/templates/ ,这也就是为什么你在 Controller 中只返回了一个名称,它却能够找到对应的文件。
    resolver.setSuffix(this.properties.getSuffix());      // 这里默认使用 .html,不过所有能从 properties 中取出来的值,都能通过 xxx.xx.xx 在 application.yml 中设置 
    resolver.setTemplateMode(this.properties.getMode());  // 这里默认为 HTML,Thymeleaf 支持三类共6种模式,在 TemplateMode 中定义
    if (this.properties.getEncoding() != null) {
        resolver.setCharacterEncoding(this.properties.getEncoding().name());
    }
    resolver.setCacheable(this.properties.isCache());
    Integer order = this.properties.getTemplateResolverOrder();
    if (order != null) {
        resolver.setOrder(order);
    }
    resolver.setCheckExistence(this.properties.isCheckTemplate());
    return resolver;
}

03-添加界面

在了解了上述流程后,给我们 SSO 登录过程中的请求增加页面就变得十分简单了。

首先,我们来给 CAS 系统增加登录界面。当我们访问业务系统时,如果未登录,则跳转到登录界面。 创建 /resources/templates/login.html 文件,并在里面增加登录界面需要的 html。 访问效果是: Spring Boot「37」用 Thymeleaf 渲染你的界面

然后,我们来给业务系统增加用户列表的显示页面。 同样,创建 /resources/templates/users.html 文件,在 /resources/css/demo.css 中增加样式内容。 当我们从 CAS 认证登录后,显示用户列表。 以下是增加页面之前、之后的对比效果,可以看出还是有界面的更让人感到满意。 Spring Boot「37」用 Thymeleaf 渲染你的界面

其中需要注意的一个问题是,我们 users.html 需要引用 css 静态资源,在 Spring Boot 中需要用 WebMvcConfigurer 配置一下静态资源的处理。

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**")
            .addResourceLocations("classpath:/");
}

04-总结

今天,我介绍了 Spring MVC 中请求访问的基本处理流程,然后重点讨论了 ViewResolver 的处理流程,以及 Spring Boot 自动化装配时对 Thymeleaf 引擎的配置。 然后,基于这些流程我为之前的 SSO 登录过程增加了显示页面,以后调试就不用再用 postman 模拟发送请求了。

希望今天的内容能对你有所帮助。

转载自:https://juejin.cn/post/7203630405711282237
评论
请登录