java框架之SpringBoot(4)-资源映射&thymeleaf

时间:2023-03-09 23:43:28
java框架之SpringBoot(4)-资源映射&thymeleaf

资源映射

静态资源映射

查看 SpringMVC 的自动配置类,里面有一个配置静态资源映射的方法:

 @Override
 public void addResourceHandlers(ResourceHandlerRegistry registry) {
     if (!this.resourceProperties.isAddMappings()) {
         logger.debug("Default resource handling disabled");
         return;
     }
     Integer cachePeriod = this.resourceProperties.getCachePeriod();
     if (!registry.hasMappingForPattern("/webjars/**")) {
         // 将路径为 "/webjars/**" 匹配到的资源在 "classpath:/META-INF/resources/webjars/"
         customizeResourceHandlerRegistration(registry
                 .addResourceHandler("/webjars/**")
                 .addResourceLocations("classpath:/META-INF/resources/webjars/")
                 .setCachePeriod(cachePeriod));
     }
     // 从配置中获取静态路由规则
     String staticPathPattern = this.mvcProperties.getStaticPathPattern();
     if (!registry.hasMappingForPattern(staticPathPattern)) {
         // 将路径为 staticPathPattern 匹配到的资源在 this.resourceProperties.getStaticLocations()
         customizeResourceHandlerRegistration(
                 registry.addResourceHandler(staticPathPattern)
                         .addResourceLocations(
                                 this.resourceProperties.getStaticLocations())
                         .setCachePeriod(cachePeriod));
     }
 }

org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addResourceHandlers

从第 8-14 行可以看到,有一个默认配置,将能匹配 "/webjars/**" 的请求路径映射到 "classpath:/META-INF/resources/webjars/" 中。

接着从 16-24 行又将 this.mvcProperties.getStaticPathPattern() 变量对应值的路径映射 this.resourceProperties.getStaticLocations() 对应值的目录下。

查看 this.mvcProperties 对应的配置类:

 /*
  * Copyright 2012-2017 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */

 package org.springframework.boot.autoconfigure.web;

 import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Map;

 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.http.MediaType;
 import org.springframework.validation.DefaultMessageCodesResolver;

 /**
  * {@link ConfigurationProperties properties} for Spring MVC.
  *
  * @author Phillip Webb
  * @author Sébastien Deleuze
  * @author Stephane Nicoll
  * @author Eddú Meléndez
  * @since 1.1
  */
 @ConfigurationProperties(prefix = "spring.mvc")
 public class WebMvcProperties {

     /**
      * Formatting strategy for message codes (PREFIX_ERROR_CODE, POSTFIX_ERROR_CODE).
      */
     private DefaultMessageCodesResolver.Format messageCodesResolverFormat;

     /**
      * Locale to use. By default, this locale is overridden by the "Accept-Language"
      * header.
      */
     private Locale locale;

     /**
      * Define how the locale should be resolved.
      */
     private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER;

     /**
      * Date format to use (e.g. dd/MM/yyyy).
      */
     private String dateFormat;

     /**
      * Dispatch TRACE requests to the FrameworkServlet doService method.
      */
     private boolean dispatchTraceRequest = false;

     /**
      * Dispatch OPTIONS requests to the FrameworkServlet doService method.
      */
     private boolean dispatchOptionsRequest = true;

     /**
      * If the content of the "default" model should be ignored during redirect scenarios.
      */
     private boolean ignoreDefaultModelOnRedirect = true;

     /**
      * If a "NoHandlerFoundException" should be thrown if no Handler was found to process
      * a request.
      */
     private boolean throwExceptionIfNoHandlerFound = false;

     /**
      * Enable warn logging of exceptions resolved by a "HandlerExceptionResolver".
      */
     private boolean logResolvedException = false;

     /**
      * Maps file extensions to media types for content negotiation, e.g. yml->text/yaml.
      */
     private Map<String, MediaType> mediaTypes = new LinkedHashMap<String, MediaType>();

     /**
      * Path pattern used for static resources.
      */
     private String staticPathPattern = "/**";

     private final Async async = new Async();

     private final Servlet servlet = new Servlet();

     private final View view = new View();

     public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
         return this.messageCodesResolverFormat;
     }

     public void setMessageCodesResolverFormat(
             DefaultMessageCodesResolver.Format messageCodesResolverFormat) {
         this.messageCodesResolverFormat = messageCodesResolverFormat;
     }

     public Locale getLocale() {
         return this.locale;
     }

     public void setLocale(Locale locale) {
         this.locale = locale;
     }

     public LocaleResolver getLocaleResolver() {
         return this.localeResolver;
     }

     public void setLocaleResolver(LocaleResolver localeResolver) {
         this.localeResolver = localeResolver;
     }

     public String getDateFormat() {
         return this.dateFormat;
     }

     public void setDateFormat(String dateFormat) {
         this.dateFormat = dateFormat;
     }

     public boolean isIgnoreDefaultModelOnRedirect() {
         return this.ignoreDefaultModelOnRedirect;
     }

     public void setIgnoreDefaultModelOnRedirect(boolean ignoreDefaultModelOnRedirect) {
         this.ignoreDefaultModelOnRedirect = ignoreDefaultModelOnRedirect;
     }

     public boolean isThrowExceptionIfNoHandlerFound() {
         return this.throwExceptionIfNoHandlerFound;
     }

     public void setThrowExceptionIfNoHandlerFound(
             boolean throwExceptionIfNoHandlerFound) {
         this.throwExceptionIfNoHandlerFound = throwExceptionIfNoHandlerFound;
     }

     public boolean isLogResolvedException() {
         return this.logResolvedException;
     }

     public void setLogResolvedException(boolean logResolvedException) {
         this.logResolvedException = logResolvedException;
     }

     public Map<String, MediaType> getMediaTypes() {
         return this.mediaTypes;
     }

     public void setMediaTypes(Map<String, MediaType> mediaTypes) {
         this.mediaTypes = mediaTypes;
     }

     public boolean isDispatchOptionsRequest() {
         return this.dispatchOptionsRequest;
     }

     public void setDispatchOptionsRequest(boolean dispatchOptionsRequest) {
         this.dispatchOptionsRequest = dispatchOptionsRequest;
     }

     public boolean isDispatchTraceRequest() {
         return this.dispatchTraceRequest;
     }

     public void setDispatchTraceRequest(boolean dispatchTraceRequest) {
         this.dispatchTraceRequest = dispatchTraceRequest;
     }

     public String getStaticPathPattern() {
         return this.staticPathPattern;
     }

     public void setStaticPathPattern(String staticPathPattern) {
         this.staticPathPattern = staticPathPattern;
     }

     public Async getAsync() {
         return this.async;
     }

     public Servlet getServlet() {
         return this.servlet;
     }

     public View getView() {
         return this.view;
     }

     public static class Async {

         /**
          * Amount of time (in milliseconds) before asynchronous request handling times
          * out. If this value is not set, the default timeout of the underlying
          * implementation is used, e.g. 10 seconds on Tomcat with Servlet 3.
          */
         private Long requestTimeout;

         public Long getRequestTimeout() {
             return this.requestTimeout;
         }

         public void setRequestTimeout(Long requestTimeout) {
             this.requestTimeout = requestTimeout;
         }

     }

     public static class Servlet {

         /**
          * Load on startup priority of the dispatcher servlet.
          */
         private int loadOnStartup = -1;

         public int getLoadOnStartup() {
             return this.loadOnStartup;
         }

         public void setLoadOnStartup(int loadOnStartup) {
             this.loadOnStartup = loadOnStartup;
         }

     }

     public static class View {

         /**
          * Spring MVC view prefix.
          */
         private String prefix;

         /**
          * Spring MVC view suffix.
          */
         private String suffix;

         public String getPrefix() {
             return this.prefix;
         }

         public void setPrefix(String prefix) {
             this.prefix = prefix;
         }

         public String getSuffix() {
             return this.suffix;
         }

         public void setSuffix(String suffix) {
             this.suffix = suffix;
         }

     }

     public enum LocaleResolver {

         /**
          * Always use the configured locale.
          */
         FIXED,

         /**
          * Use the "Accept-Language" header or the configured locale if the header is not
          * set.
          */
         ACCEPT_HEADER

     }

 }

org.springframework.boot.autoconfigure.web.WebMvcProperties

查看 this.resourceProperties 对应的配置类:

/*
 * Copyright 2012-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure.web;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

/**
 * Properties used to configure resource handling.
 *
 * @author Phillip Webb
 * @author Brian Clozel
 * @author Dave Syer
 * @author Venil Noronha
 * @since 1.1.0
 */
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware, InitializingBean {

    private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/" };

    private static final String[] RESOURCE_LOCATIONS;

    static {
        RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
                + SERVLET_RESOURCE_LOCATIONS.length];
        System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
                SERVLET_RESOURCE_LOCATIONS.length);
        System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
                SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
    }

    /**
     * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
     * /resources/, /static/, /public/] plus context:/ (the root of the servlet context).
     */
    private String[] staticLocations = RESOURCE_LOCATIONS;

    /**
     * Cache period for the resources served by the resource handler, in seconds.
     */
    private Integer cachePeriod;

    /**
     * Enable default resource handling.
     */
    private boolean addMappings = true;

    private final Chain chain = new Chain();

    private ResourceLoader resourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void afterPropertiesSet() {
        this.staticLocations = appendSlashIfNecessary(this.staticLocations);
    }

    public String[] getStaticLocations() {
        return this.staticLocations;
    }

    public void setStaticLocations(String[] staticLocations) {
        this.staticLocations = appendSlashIfNecessary(staticLocations);
    }

    private String[] appendSlashIfNecessary(String[] staticLocations) {
        String[] normalized = new String[staticLocations.length];
        for (int i = 0; i < staticLocations.length; i++) {
            String location = staticLocations[i];
            if (location != null) {
                normalized[i] = (location.endsWith("/") ? location : location + "/");
            }
        }
        return normalized;
    }

    public Resource getWelcomePage() {
        for (String location : getStaticWelcomePageLocations()) {
            Resource resource = this.resourceLoader.getResource(location);
            try {
                if (resource.exists()) {
                    resource.getURL();
                    return resource;
                }
            }
            catch (Exception ex) {
                // Ignore
            }
        }
        return null;
    }

    private String[] getStaticWelcomePageLocations() {
        String[] result = new String[this.staticLocations.length];
        for (int i = 0; i < result.length; i++) {
            String location = this.staticLocations[i];
            if (!location.endsWith("/")) {
                location = location + "/";
            }
            result[i] = location + "index.html";
        }
        return result;
    }

    List<Resource> getFaviconLocations() {
        List<Resource> locations = new ArrayList<Resource>(
                this.staticLocations.length + 1);
        if (this.resourceLoader != null) {
            for (String location : this.staticLocations) {
                locations.add(this.resourceLoader.getResource(location));
            }
        }
        locations.add(new ClassPathResource("/"));
        return Collections.unmodifiableList(locations);
    }

    public Integer getCachePeriod() {
        return this.cachePeriod;
    }

    public void setCachePeriod(Integer cachePeriod) {
        this.cachePeriod = cachePeriod;
    }

    public boolean isAddMappings() {
        return this.addMappings;
    }

    public void setAddMappings(boolean addMappings) {
        this.addMappings = addMappings;
    }

    public Chain getChain() {
        return this.chain;
    }

    /**
     * Configuration for the Spring Resource Handling chain.
     */
    public static class Chain {

        /**
         * Enable the Spring Resource Handling chain. Disabled by default unless at least
         * one strategy has been enabled.
         */
        private Boolean enabled;

        /**
         * Enable caching in the Resource chain.
         */
        private boolean cache = true;

        /**
         * Enable HTML5 application cache manifest rewriting.
         */
        private boolean htmlApplicationCache = false;

        /**
         * Enable resolution of already gzipped resources. Checks for a resource name
         * variant with the "*.gz" extension.
         */
        private boolean gzipped = false;

        @NestedConfigurationProperty
        private final Strategy strategy = new Strategy();

        /**
         * Return whether the resource chain is enabled. Return {@code null} if no
         * specific settings are present.
         * @return whether the resource chain is enabled or {@code null} if no specified
         * settings are present.
         */
        public Boolean getEnabled() {
            return getEnabled(getStrategy().getFixed().isEnabled(),
                    getStrategy().getContent().isEnabled(), this.enabled);
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public boolean isCache() {
            return this.cache;
        }

        public void setCache(boolean cache) {
            this.cache = cache;
        }

        public Strategy getStrategy() {
            return this.strategy;
        }

        public boolean isHtmlApplicationCache() {
            return this.htmlApplicationCache;
        }

        public void setHtmlApplicationCache(boolean htmlApplicationCache) {
            this.htmlApplicationCache = htmlApplicationCache;
        }

        public boolean isGzipped() {
            return this.gzipped;
        }

        public void setGzipped(boolean gzipped) {
            this.gzipped = gzipped;
        }

        static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled,
                Boolean chainEnabled) {
            return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled;
        }

    }

    /**
     * Strategies for extracting and embedding a resource version in its URL path.
     */
    public static class Strategy {

        @NestedConfigurationProperty
        private final Fixed fixed = new Fixed();

        @NestedConfigurationProperty
        private final Content content = new Content();

        public Fixed getFixed() {
            return this.fixed;
        }

        public Content getContent() {
            return this.content;
        }

    }

    /**
     * Version Strategy based on content hashing.
     */
    public static class Content {

        /**
         * Enable the content Version Strategy.
         */
        private boolean enabled;

        /**
         * Comma-separated list of patterns to apply to the Version Strategy.
         */
        private String[] paths = new String[] { "/**" };

        public boolean isEnabled() {
            return this.enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public String[] getPaths() {
            return this.paths;
        }

        public void setPaths(String[] paths) {
            this.paths = paths;
        }

    }

    /**
     * Version Strategy based on a fixed version string.
     */
    public static class Fixed {

        /**
         * Enable the fixed Version Strategy.
         */
        private boolean enabled;

        /**
         * Comma-separated list of patterns to apply to the Version Strategy.
         */
        private String[] paths = new String[] { "/**" };

        /**
         * Version string to use for the Version Strategy.
         */
        private String version;

        public boolean isEnabled() {
            return this.enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public String[] getPaths() {
            return this.paths;
        }

        public void setPaths(String[] paths) {
            this.paths = paths;
        }

        public String getVersion() {
            return this.version;
        }

        public void setVersion(String version) {
            this.version = version;
        }

    }

}

org.springframework.boot.autoconfigure.web.ResourceProperties

即 16-24 行就是将能匹配 "/**"  的请求路径映射到项目路径下 "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" 中。

结论:

  • 请求路径如果匹配 "/webjars/**" 规则,那么 SpringBoot 就会去 classpath:/META-INF/resources/webjars/ 目录下寻找对应资源。
  • 请求路径如果匹配 "/**" 规则(即任意请求路径),那么 SpringBoot 就会去 "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" 目录下寻找对应资源。

欢迎页

依旧是 SpringMVC 配置类中,有一个注册欢迎页映射 bean 的方法:

 @Bean
 public WelcomePageHandlerMapping welcomePageHandlerMapping(
         ResourceProperties resourceProperties) {
     return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
             this.mvcProperties.getStaticPathPattern());
 }

org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#welcomePageHandlerMapping

查看 resourceProperties.getWelcomePage() 方法:

 public Resource getWelcomePage() {
     for (String location : getStaticWelcomePageLocations()) {
         Resource resource = this.resourceLoader.getResource(location);
         try {
             if (resource.exists()) {
                 resource.getURL();
                 return resource;
             }
         }
         catch (Exception ex) {
             // Ignore
         }
     }
     return null;
 }

org.springframework.boot.autoconfigure.web.ResourceProperties#getWelcomePage

接着查看 getStaticWelcomePageLocations() 方法:

 private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };

 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
             "classpath:/META-INF/resources/", "classpath:/resources/",
             "classpath:/static/", "classpath:/public/" };

 private static final String[] RESOURCE_LOCATIONS;

 static {
     RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length
             + SERVLET_RESOURCE_LOCATIONS.length];
     System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
             SERVLET_RESOURCE_LOCATIONS.length);
     System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
             SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
 }

 private String[] staticLocations = RESOURCE_LOCATIONS;

 private String[] getStaticWelcomePageLocations() {
     String[] result = new String[this.staticLocations.length];
     for (int i = 0; i < result.length; i++) {
         String location = this.staticLocations[i];
         if (!location.endsWith("/")) {
             location = location + "/";
         }
         result[i] = location + "index.html";
     }
     return result;
 }

org.springframework.boot.autoconfigure.web.ResourceProperties#getStaticWelcomePageLocations

即 resourceProperties.getWelcomePage() 方法默认就是从静态资源目录下即 "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" 目录中寻找名为 "index.html" 的资源。

结论:

  • SpringBoot 中默认的欢迎页为 "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" 目录下名为的 "index.html" 的页面。

页面图标

在 SpringMVC 配置类中还有一个页面图标配置类:

 @Configuration
 @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)  // 默认开启图标显示
 public static class FaviconConfiguration {

     private final ResourceProperties resourceProperties;

     public FaviconConfiguration(ResourceProperties resourceProperties) {
         this.resourceProperties = resourceProperties;
     }

     @Bean
     public SimpleUrlHandlerMapping faviconHandlerMapping() {
         SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
         mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
         mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
                 faviconRequestHandler()));
         return mapping;
     }

     @Bean
     public ResourceHttpRequestHandler faviconRequestHandler() {
         ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
         requestHandler
                 .setLocations(this.resourceProperties.getFaviconLocations());
         return requestHandler;
     }

 }

org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.FaviconConfiguration

第 12 行的 FaviconConfiguration 方法便是用来处理图标映射,在第 15 行为匹配 "**/favicon.ico" 的请求路径指定了图标请求处理器 faviconRequestHandler() ,在第 24 行设置了图标请求处理器寻找图标的目录为 this.resourceProperties.getFaviconLocations() ,查看该方法:

 List<Resource> getFaviconLocations() {
     List<Resource> locations = new ArrayList<Resource>(
             this.staticLocations.length + 1);
     if (this.resourceLoader != null) {
         for (String location : this.staticLocations) {
             locations.add(this.resourceLoader.getResource(location));
         }
     }
     locations.add(new ClassPathResource("/"));
     return Collections.unmodifiableList(locations);
 }

org.springframework.boot.autoconfigure.web.ResourceProperties#getFaviconLocations

可以看到,该方法返回的是静态文件夹目录资源。

结论:

  • 在 SpringBoot 工程中的静态资源目录放置一个名为 "favicon.ico" 的网页图标,该图标就会被 SpringBoot 使用。

模板引擎thymeleaf

thymeleaf中文离线文档下载(提取码:ip1g) | thymeleaf官网

引入

thymeleaf 是 SpringBoot 推荐使用的一款模板引擎框架,要引入很简单,SpringBoot 为它提供了场景启动器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

切换版本

如 SpringBoot 1.5.19 版本使用的 thymeleaf 版本默认为 2.1.6,如果想切换到 3.0 以上,直接覆盖它的版本定义属性即可,要注意的是需要同时更新它的布局功能支持程序的版本:

<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<!--布局功能支持程序,thymeleaf 使用 3.0 版本以上时支持程序要使用 2.0 以上-->
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>

SpringBoot中使用

要在 SpringBoot 中使用 thymeleaf,可以先看下 thymeleaf 的自动配置类:

@Configuration
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass(SpringTemplateEngine.class)
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class ThymeleafAutoConfiguration {

查看它的属性映射类:

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

    private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");

    private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");

    public static final String DEFAULT_PREFIX = "classpath:/templates/";

    public static final String DEFAULT_SUFFIX = ".html";

一目了然,thymeleaf 默认使用的模板路径为 classpath:/templates/ ,且可省略后缀 .html ,下面我们就开始在 SpringBoot 项目中使用 thymeleaf:

1、创建测试控制器:

package com.springboot.webdev1;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class TestController {

    @RequestMapping("test")
    public String test(Model model){
        // 传值
        model.addAttribute("name", "张三");
        // SpringBoot 会找到 classpath:templates/test.html 使用 thymeleaf 渲染
        return "test";
    }
}

com.springboot.webdev1.TestController

2、新建模板页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
<h1 th:text="${name}"></h1>
</body>
</html>

templates/test.html

3、测试:

启动项目,访问 localhost:8080/test:

java框架之SpringBoot(4)-资源映射&thymeleaf

test

IDEA语法报错解决

关闭 thymeleaf 的表达式语法检查:

java框架之SpringBoot(4)-资源映射&thymeleaf

热部署

这里选用的是 Idea 工具进行操作,thymeleaf 的实时变更依赖于此 IDE。

1、导入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

2、开启 Idea 的自动编译,也可以通过 Ctrl+F9 手动编译:

java框架之SpringBoot(4)-资源映射&thymeleaf

3、事件设置,让 thymeleaf 变更即时生效, Ctrl+Shift+A 打开时间对话框,选择勾选如下:

java框架之SpringBoot(4)-资源映射&thymeleaf

java框架之SpringBoot(4)-资源映射&thymeleaf

标准表达式

变量表达式

thymeleaf 的变量表达式类似于 EL 表达式,通过 ${} 取值。

List<String> nameList = new ArrayList<>();
nameList.add("张三");
nameList.add("李四");
nameList.add("王五");

model.addAttribute("name", "张三");
model.addAttribute("nameList", nameList);

controller

<!--取值-->
<span th:text="${name}"></span>
<hr>
<!--循环-->
<ul>
    <li th:each="name : ${nameList}"><span th:text="${name}"/></li>
</ul>

java框架之SpringBoot(4)-资源映射&thymeleaf

html

选择变量表达式

选择变量表达式很像,不同它需要预先选择一个对象作为上下文变量容器。

public class User {
    public User(){}
    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    private String name;

    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

com.springboot.webdev1.bean.User

model.addAttribute("user", new User("张三", 18));

controller

<div th:object="${user}">
    <p>姓名:<span th:text="*{name}"></span></p>
    <p>年龄:<span th:text="*{age}"></span></p>
</div>

html

URL表达式

URL 表达式可以帮助我们更轻松的动态拼装请求 URL。

<!--() 中可以指定要传递的参数-->
<span th:text="@{/order/details(type=1,keyword=ff)}"></span>

html

表达式支持语法

字面量

文本文字 : 'one text', 'Another one!',…
数字文本 : 0, 34, 3.0, 12.3,…
布尔文本 : true, false
空 : null
文字标记 : one, sometext, main,…

文本操作

字符串连接 : +
文本替换 : |The name is ${name}|

算术运算

二元运算符 : +, -, *, /, %
减号(单目运算符) : -

布尔操作

二元运算符 : and, or
布尔否定(一元运算符) : !, not

比较

比较 : >, <, >=, <= (gt, lt, ge, le)
等值运算符 :==, != (eq, ne)

条件运算

If-then : (if) ? (then)  # 例:<span th:text="${name} == '张三' ? 'Administrator'"/>
If-then-else : (if) ? (then) : (else)  # 例:<span th:text="${name} == '张三' ? 'Administrator' : (${name} ?: 'Unknown')"/>
Default : (value) ?: (defaultvalue)  # 例:<span th:text="${name} ?: 'Unknown'"/>

常用标签

关键字 功能介绍 案例
th:id 替换id  <input th:id="'xxx' + ${collect.id}"/> 
th:text 文本替换  <p th:text="${collect.description}">description</p> 
th:utext 支持html的文本替换  <p th:utext="${htmlcontent}">conten</p> 
th:object 替换对象  <div th:object="${session.user}"> 
th:value 属性赋值  <input th:value="${user.name}" /> 
th:with 变量赋值运算  <div th:with="isEven=${prodStat.count}%2==0"></div> 
th:style 设置样式  <span th:style="'display:' + @{(${sitrue} ? 'none' : 'inline-block')} + ''"/>  
th:onclick 点击事件  <button th:onclick="'getCollect()'"></button> 
th:each 属性赋值  <tr th:each="user,userStat:${users}"></tr>  
th:if 判断条件  <a th:if="${userId == collect.userId}" > 
th:unless 和th:if判断相反
<a th:href="@{/login}" th:unless=${session.user != null}>Login</a>
th:href 链接地址
<a th:href="@{/login}" th:unless=${session.user != null}>Login</a>
th:switch 多路选择 配合th:case 使用  <div th:switch="${user.role}"> 
th:case th:switch的一个分支  <p th:case="'admin'">User is an administrator</p> 
th:fragment 布局标签,定义一个代码片段,方便其它地方引用  <div th:fragment="alert"> 
th:include 布局标签,替换内容到引入的文件  <head th:include="layout :: htmlhead" th:with="title='xx'"></head> /> 
th:replace 布局标签,替换整个标签到引入的文件  <div th:replace="fragments/header :: title"></div> 
th:selected selected选择框 选中  <option th:selected="(${xxx.id} == ${configObj.dd})"></option> 
th:src 图片类地址引入  <img class="img-responsive" alt="App Logo" th:src="@{/img/logo.png}" /> 
th:inline 定义js脚本可以使用变量  <script type="text/javascript" th:inline="javascript"> 
th:action 表单提交的地址  <form action="subscribe.html" th:action="@{/subscribe}"> 
th:remove 删除某个属性

 <tr th:remove="all">  

1.all:删除包含标签和所有的孩子。2.body:不包含标记删除,但删除其所有的孩子。3.tag:包含标记的删除,但不删除它的孩子。4.all-but-first:删除所有包含标签的孩子,除了第一个。5.none:什么也不做。这个值是有用的动态评估。

th:attr 设置标签属性,多个属性可以用逗号分隔

<img th:attr="src=@{/image/aa.jpg},title=#{logo}"/> 此标签不太优雅,一般用的比较少。

一个标签内可以包含多个th:x属性,其生效优先级顺序如下:

include、each、if/unless/switch/case、with、attr、attrprepend、attrappend、value、href、src、etc、text、utext、fragment、remove

常用操作

字符串拼接

<!--使用 + 号-->
<span th:text="'Welcome to our application, ' + ${name} + '!'"/> <br>
<!--使用 | 进行字符串格式化-->
<span th:text="|Welcome to our application, ${name}!|"/>

条件判断

<span th:if="${name}=='张三'">是张三</span>
<span th:unless="${name}=='张三'">不是张三</span>
<span th:text="${name} ?: 'Unknown'"/>
<span th:text="${name} == '张三' ? 'Administrator'"/>
<span th:text="${name} == '张三' ? 'Administrator' : (${name} ?: 'Unknown')"/>
<div th:switch="${name}">
    <span th:case="张三">name 为张三</span>
    <span th:case="李四">name 为李四</span>
</div>

循环

<ul>
    <li th:each="name,iterStat : ${nameList}" th:text="${iterStat.count} + ':'+ ${name}"></li>
</ul>
<!--
iterStat称作状态变量,属性有:
    index:当前迭代对象的index(从0开始计算)
    count: 当前迭代对象的index(从1开始计算)
    size:被迭代对象的大小
    current:当前迭代变量
    even/odd:布尔值,当前循环是否是偶数/奇数(从0开始计算)
    first:布尔值,当前循环是否是第一个
    last:布尔值,当前循环是否是最后一个
-->

组装URL

<!--() 中可以指定要传递的参数-->
<form th:action="@{/order/details(type=1,keyword=ff)}" ></form>
<!--上述对应的 URL 为 /order/details?type=1&keyword=ff-->

常用内置对象

thymeleaf 为我们提供了很多内置对象,通过 ${#内置对象名称} 即可访问到,下面列出一些比较常用的:

内置对象 作用 示例
dates 日期操作
<span th:text="${#dates.format(currentDate,'yyyy-MM-dd HH:mm:ss')}"/>
<!--格式化日期-->
numbers 数字格式化
<span th:text="${#numbers.formatDecimal(13.213, 0, 2)}"></span>
<!--此示例表示保留两位小数位,整数位自动  结果 13.21-->
<span th:text="${#numbers.formatDecimal(13.213, 3, 2)}"></span>
<!--此示例表示保留两位小数位,3位整数位(不够的前加0) 结果 013.21-->
lists 列表操作
<p th:text="${#lists.size(nameList)}"/>
<!--获取列表长度-->
calendars 日历操作
<p th:text="${#calendars.format(#calendars.createNow(),'yyyy-MM-dd HH:mm:ss')}"></p>
<!--格式化日期,与 #dates 相似-->
strings 字符串操作 
<p th:text="${#strings.startsWith('abcde','aab')}"/>
<!--判断字符串是否以指定字符串开头-->
objects 对象操作
<p th:text="${#objects.nullSafe(name,'Unknown')}"></p>
<!--判断指定对象是否为空,如果是空则返回指定默认值,否则原样返回-->
bools 布尔值操作
<p th:text="${#bools.isFalse(1>2)}">aa</p>
<!--判断一个表达式结果是否为假-->
arrays 数组操作
<p th:text="${#arrays.isEmpty(testArr)}"></p>
<!--判断一个数组是否为空-->
sets 集合操作
<p th:text="${#sets.size(set)}"></p>
<!--获取一个集合中元素个数-->
maps 地图操作
<p th:text="${#maps.containsKey(map,'key1')}"></p>
<!--判断一个 Map 中是否存在指定 key-->
aggregates 统计运算
<p th:text="${#aggregates.avg(numArr)}"></p>
<!--计算一个数组中的平均值-->
messages 属性文件取值 
<p th:text="${#messages.msg('hahah')}"/>
<!--取一个属性文件中的属性值,相当于 <p th:text="#{hahah}"/>-->
convertions 类型转换
<p th:text="${#conversions.convert('213','java.lang.Integer')+23}"></p>
<!--将一个字符串转成 Integer 类型-->
execInfo 模板信息
<p th:text="${#execInfo.getTemplateName()}"></p>
<!--获取运行时当前模板名称-->
request 请求对象
<p th:text="${#request.method}"></p>
<!--通过请求对象获取当前的请求方法 #httpServletRequest 与之相同-->
response 响应对象
<p th:text="${#response.getWriter().write('aaa')}"/>
<!--通过相应对象输出字符串 -->
session 会话对象
<p th:text="${#session.getId()}"></p>
<!--通过会话对象获取当前会话id #httpSession 与之相同-->

布局

介绍

在 web 开发中,我们经常会将公共头,公共尾,菜单等部分提取成模板供其它页面使用。在 thymeleaf 中,通过 th:fragment、th:include、th:replace、参数化模板配置、css 选择器加载代码块等实现。

依赖

Spring Boot 2.0 将布局单独提取了出来,需要单独引入依赖:thymeleaf-layout-dialect。

<dependency>
    <groupId>nz.net.ultraq.thymeleaf</groupId>
    <artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>

选择器使用

1、定义模板:

<div class="header">
    这是头部
</div>

templates/common/header.html

<div class="body">
    这是主体
</div>

templates/common/body.html

<div class="footer">
    这是尾部
</div>

templates/common/footer.html

2、引用模板:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>模板测试</title>
</head>
<body>
    <!--insert 会将所有选择的标签及内容插入到当前标签内-->
    <div class="layout_header" th:insert="common/header :: .header"></div>
    <!--replace 会让选择的标签替换当前的标签-->
    <div class="layout_body" th:replace="common/body :: .body"></div>
    <!--include 会将选择的标签内容插入到当前标签内-->
    <div class="layout_footer" th:include="common/footer :: .footer"></div>
</body>
</html>

java框架之SpringBoot(4)-资源映射&thymeleaf

templates/layout.html

fragment使用

1、定义模板块:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>fragment Test</title>
</head>
<body>
<!--fragment 定义用于被加载的块-->
<span th:fragment="copy">msg from fragment</span>
<!--定义能接收参数的块-->
<span th:fragment="sayHello(msg, name)">[[|${msg} ${name}|]]</span>
</body>
</html>

templates/common/fragment.html

2、使用模板块:  

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>模板测试</title>
</head>
<body>
<div th:include="common/fragment::copy"></div>
<div th:include="common/fragment::sayHello('hello','bob')"></div>
</body>
</html>

java框架之SpringBoot(4)-资源映射&thymeleaf

templates/layout.html

其中 th:include 、 th:insert 、 th:replace 中的参数格式为 templatename::[domselector] ,其中 templatename 是模板名(如 footer ), domselector 是可选的 dom 选择器。如果只写 templatename ,不写 domselector ,则会加载整个模板。