統合Thymeleafの使用マニュアルのSpringBootシリーズ

SpringBootシリーズThymeleaf文法簡潔な

@
このブログは、より重要な点は見ての挑戦の一部だけがあるので、Thymeleaf公式文書だけでなく、使用してSpringBootを統合方法を説明し、より詳細な説明があります

1、テンプレートエンジン

リファレンステンプレートエンジンBaiduの百科事典は説明しました:

テンプレートエンジンは、(ここでは特にWeb開発テンプレートエンジン用)の分離に起因するユーザーインターフェースとビジネスデータ(コンテンツ)を作ることです、それはウェブサイトのテンプレートエンジンのために、特定の形式の文書を生成することができ、標準を生成します。 HTMLドキュメント。

そこJavaEEの中にいくつかの領域、より一般的に使用されるテンプレートエンジン、つまりJSP、速度、Freemarkerの、Thymeleafが、効率、JSPをレンダリングするためのフロントページのためであるが、それはまだ最速で、ベロシティが続きました。Thymeleaf効率のレンダリング中には非常に高速ではありませんが、文法がよりコンパクトで、ベロシティThymeleaf文法よりも軽いが、速度よりも効率レンダリング

2、Thymeleafのプロフィール

2.1)、Thymeleafは、定義されました

Thymeleafは、WebとHTML、XML、JavaScriptやCSS、あるいはプレーンテキストを扱うことができる近代的なサーバーサイドJavaのテンプレートエンジンのための独立した環境です。特定の参照Thymeleaf公式サイト

公式サイトでは、オンラインドキュメントをファイル形式で提供された文書をご用意しております
ここに画像を挿入説明

2.2)、テンプレートを適用

Thymeleafは、次のテンプレートに適用されます。

  • HTML
  • XML
  • テキスト
  • JAVASCRIPT
  • CSS

テンプレート(HTMLやXML)をマークする二つのモード、3つのテキストテンプレートモード(TEXT、JavaScriptとCSS)テンプレートと非動作モード(RAW)があります。

[OK]を、ここではより多くの重要なポイントをいくつか紹介します

3、重要なポイント

3.1)、目:テキスト和番目:のutext

これは、2つのタイプに分けThymeleafセットのテキストラベルは、一つは目で非常に一般的に使用されるテキストタグ、次のとおりです。テキストは、別の目である:のutextは、二つの最も重要な違いは、特殊文字の意志がありますエスケープ

  • 目:テキスト:すべての文字特殊文字がに変わり
  • 目:のutext:文字は特殊文字をエスケープしません

注:これは、主にこれらの文字がサポートされていないなど、HTMLタグ、/ N、/トンの特殊文字を指します。
比較例を書きます:

<span th:text="${'Welcome to our <b>fantastic</b> grocery store!'}"></span><br/>
<span th:utext="${'Welcome to our <b>fantastic</b> grocery store!'}"></span>

ここに画像を挿入説明

3.2)、標準的な発現

標準式の構文この章は、式の構文は、標準的なアプリケーションで記述し、公式文書があります

  • 単純式(シンプルな表現の構文):
    • 変数式:$ {...} // GETトラバース値、サポートOGNL文法など
    1. カスタムオブジェクトのプロパティ値を取得します
    2. カスタム変数属性の値を取得します。
    3. 組み込みの基本的なオブジェクトを使用します
      • CTX:コンテキストオブジェクト。

      • varsは:コンテキスト変数。

      • ロケール:コンテキストのロケール。

      • リクエスト:(唯一のWebコンテキストで)HttpServletRequestオブジェクト。

      • 応答:HttpServletResponseオブジェクト(のみのWebコンテキストで)。

      • セッション:HttpSessionオブジェクト(のみのWebコンテキストで)。

      • ServletContext:ServletContextオブジェクト(のみのWebコンテキストで)。

      詳細についてを参照してくださいThymeleaf付録A
    4. 組み込みツールは、オブジェクト
      の公式ウェブサイトは、詳細な使用方法を参照して、より詳細な説明を与えてきたThymeleaf付録B
      ここに画像を挿入説明
<link th:href="@{/static/css/public.css}" rel="stylesheet" type="text/css" />
    <link th:href="@{/static/css/index.css}" rel="stylesheet" type="text/css" />
    <script type="text/javascript" th:src="@{/static/js/jquery-1.3.2.min.js}"></script>
    <script type="text/javascript" th:src="@{/static/js/html5.js}"></script>
    <script type="text/javascript" th:src="@{/static/js/popbox.js}"></script>
* Fragment Expressions: ~{...} //片段引用的表达式 eg: `<div th:insert="~{commons :: main}">....</div>`
  • リテラル(リテラル値)
    • テキストリテラル:「1文字」、「もう1!」、...
    • 数値リテラル:0、34、3.0、12.3、...
    • ブールリテラル:真、偽
    • ヌルリテラル:ヌル
    • リテラルトークン:1、sometext、メイン、...
  • テキストの操作(テキスト操作):
    • 文字列の連結:+ //オペレーションに参加 @{url/}+${id}
    • リテラルの置換:|名前は$です {名前} | // 文字列で使用される${name}変数の値
  • 算術演算:(数学)
    • 二項演算子:+、 - 、*、/、%
    • マイナス記号(単項演算子): -
  • ブール演算ブール演算:()
    • 二項演算子:AND、OR
    • ブール否定(単項演算子):!、ありません
  • 比較と平等:(比較演算)
    • Comparators: >, <, >=, <= (gt, lt, ge, le)
    • Equality operators: ==, != (eq, ne)
  • Conditional operators:(条件运算,包括三元运算符etc.)
    • If-then: (if) ? (then)
    • If-then-else: (if) ? (then) : (else)
    • Default: (value) ?: (defaultvalue)
  • Special tokens:(特殊的令牌,也就是使用No-Operatio)
    • No-Operation: _
      ここに画像を挿入説明

      All these features can be combined and neste:
      'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

翻译过来意思是,这些语法都可以组合使用,这个章节是Thymeleaf一个重要的基本使用章节,本博客对一些重要的知识点进行摘录同时介绍一下在SpringBoot里如何使用,当然自然没有官方文档详细的,不过官方并没有通过中文文档,英文水平不好的话,阅读起来比较困难,当然我也找了一篇国内翻译过来的Thymeleaf中文文档,读者详细的可以参考文档

3.3)、Thymeleaf遍历

遍历是Thymeleaf很常用的例子,支持的属性值有:
ここに画像を挿入説明
下面还是给下例子,比较容易理解,如下代码使用th:each,th:each="item : ${items}"

<!--最新上架-->
        <div class="first-pannel clearfix">
            <div class="index-f clearfix">
                <h3 class="index-f-head"> 最新上架 <span>每天都有上新,每天都有惊喜</span> </h3>
                <div class="index-f-body">
                    <div class="top-sales newProduct">
                        <ul class="top-sales-list clearfix">
                            <li class="top-sales-item newProduct" th:each="item : ${items}">
                                <p class="item-img"> <a th:href="@{'/portal/item/toDetail/'+${item.spuId}+'/'+${item.skuId}}"><img th:src="@{${item.imgPath}}" /></a> </p>
                                <p class="item-buss"><a th:href="@{'/portal/item/toDetail/'+${item.spuId}+'/'+${item.skuId}}"></a></p>
                                <p class="item-name spec"><a th:href="@{'/portal/item/toDetail/'+${item.spuId}+'/'+${item.skuId}}" th:text="${item.itemName}"></a></p>
                                <p class="item-price spec"><em th:text="${item.mPrice}"></em>元</p>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
        <!--最新上架//-->

3.4)、公共模块抽取

在项目开发中经常遇到一些可以重用的页面,这时候就可以Thymeleaf的Template Layout进行公共页面的复用

本博客以官方介绍的复用footer.html页面进行说明
ここに画像を挿入説明
使用步骤:

  1. 抽取公共的片段
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <body>
  
    <div th:fragment="copy">
      &copy; 2011 The Good Thymes Virtual Grocery
    </div>
  
  </body>
  
</html>
  1. 引入公共的片段
    <div th:insert="~{footer :: copy}"></div>
    ~{templatename::selector}:模板名::选择器
    ~{templatename::fragmentname}:模板名::片段名
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div

三种引入公共片段的th属性:

  • th:insert:将公共片段整个插入到声明引入的元素中
  • th:replace:将声明引入的元素替换为公共片段
  • th:include:将被引入的片段的内容包含进这个标签中

效果对比:

<div>
    <footer>
    &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
</div>
<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
<div>
&copy; 2011 The Good Thymes Virtual Grocery
</div

3.5)、行内写法介绍

所谓行内写法就是没写在html对应的标签里的写法,直接在页面空白处,用[[....]]或者[(....)]的写法,然后[[....]]和[(....)]的区别其实就等同于th:text和th:utext的区别,一个会进行转义,一个不会转义特殊字符

  • [[....]]写法:会转义html标签这些特殊字符(转成字符)
  • [(....)]写法:不会转义html标签这些特殊字符(按照其原意)
    写个例子就明白了:
    ここに画像を挿入説明
<span>
    The message is [[${msg}]]
</span>
<br/>
<span>
    The message is [(${msg})]
</span>

ここに画像を挿入説明

3.6)、Thymeleaf语法规则

引用尚桂谷老师的归纳:
ここに画像を挿入説明

4、SpringBoot集成

4.1)、Springboot集成Thymeleaf简介

maven配置
因为引入了SpringBoot的parent工程,所以不需要写版本号

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

application.yml配置
注意:这里的属性大部分都可以不配置的,因为Springboot的自动配置因为做了很多自动配置,我们不配置,就使用默认的,不过下面的例子只是给读者了解一下有这些配置

#添加Thymeleaf配置,除了cache在项目没上线前建议关了,其它配置都可以不用配的,本博客只是列举一下有这些配置
  thymeleaf:
    # cache默认开启的,这里可以关了,项目上线之前,项目上线后可以开启
    cache: false
    # 这个prefix可以注释,因为默认就是templates的,您可以改成其它的自定义路径
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML5
    # 指定一下编码为utf8
    encoding: UTF-8
    # context-type为text/html,也可以不指定,因为boot可以自动识别
    content-type: text/html

Springbootが自動的に設定するには、私たちのために多くのことを行っているので、[OK]を、Springboot Thymeleafで非常に使いやすい、実際には、YAMLは、新しいフォルダのテンプレートを作成するには、以下のあなたが直接リソースにリソースを使用することができ、対応するjarファイルの直接導入を設定する必要はありませんフォルダは、すべてのHTMLファイルがある静的リソースファイルで失われ、ここで失われているにもリソースのリソースは、以下のフォルダ

新しいHTMLファイルを作成し、メモを追加<html xmlns:th="http://www.thymeleaf.org">

注Thymeleafの構文の要件はより厳格<meta charset="utf-8" >でない可能書かれたとして、それは、スラッシュを追加する必要があります<meta charset="utf-8" />

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div>
    <span th:text="${'Welcome to our <b>fantastic</b> grocery store!'}"></span><br/>
    <span th:utext="${'Welcome to our <b>fantastic</b> grocery store!'}"></span>
</div>


<span>
    The message is [[${msg}]]
</span>
<br/>
<span>
    The message is [(${msg})]
</span>
</body>
</html>

4.2)、Thymeleaf自動的に設定ソースコード解析シンプル

[OK]を、なぜ私が直接対応ポンポン構成の導入を直接使用することができることを言うのですか?Springbootは、ソースコードのシンプルな表情で自動設定SpringbootThymeleafを見て、自動的にプロジェクトに構成された多くのので、このブログをやっているので

クラスThymeleafAutoConfigurationでSpringBoot自動設定


package org.springframework.boot.autoconfigure.thymeleaf;
....

@Configuration(proxyBeanMethods = false)//定义这是一个配置类
@EnableConfigurationProperties(ThymeleafProperties.class)//使用ThymeleafProperties属性类的属性
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })//指定TemplateMode、SpringTemplateEngine(模板引擎类)起效的情况,整个配置类才起作用
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })//必须在WebMvcAutoConfiguration(SpringMVC自动配置类,这个配置类会加载组装所有的视图解析器)、WebFluxAutoConfiguration类起效后,这个Thymeleaf自动配置类才起效
public class ThymeleafAutoConfiguration {

    //没有自定义的模板解析器类的情况,使用默认的模板解析器
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(name = "defaultTemplateResolver")
    static class DefaultTemplateResolverConfiguration {

        private static final Log logger = LogFactory.getLog(DefaultTemplateResolverConfiguration.class);
        //Thymeleaf的properties配置
        private final ThymeleafProperties properties;

        private final ApplicationContext applicationContext;

        DefaultTemplateResolverConfiguration(ThymeleafProperties properties, ApplicationContext applicationContext) {
            this.properties = properties;
            this.applicationContext = applicationContext;
        }
        //用PostConstruct注解,在依赖注入完成之后,实现类的初始化配置,这个方法主要是检查模板引擎的资源文件路径是否有
        @PostConstruct
        void checkTemplateLocationExists() {
            boolean checkTemplateLocation = this.properties.isCheckTemplateLocation();
            if (checkTemplateLocation) {
                TemplateLocation location = new TemplateLocation(this.properties.getPrefix());
                if (!location.exists(this.applicationContext)) {
                    logger.warn("Cannot find template location: " + location + " (please add some templates or check "
                            + "your Thymeleaf configuration)");
                }
            }
        }
        //默认的Thymeleaf资源解析器
        @Bean
        SpringResourceTemplateResolver defaultTemplateResolver() {
            SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
            //资源解析器的所有配置
            resolver.setApplicationContext(this.applicationContext);
            resolver.setPrefix(this.properties.getPrefix());
            resolver.setSuffix(this.properties.getSuffix());
            resolver.setTemplateMode(this.properties.getMode());
            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;
        }

    }
    //又是Thymeleaf的自动配置,自动配置模板引擎SpringTemplateEngine
    @Configuration(proxyBeanMethods = false)
    protected static class ThymeleafDefaultConfiguration {

        @Bean
        @ConditionalOnMissingBean(ISpringTemplateEngine.class)
        SpringTemplateEngine templateEngine(ThymeleafProperties properties,
                ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
            SpringTemplateEngine engine = new SpringTemplateEngine();
            engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
            engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
            templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
            dialects.orderedStream().forEach(engine::addDialect);
            return engine;
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
    static class ThymeleafWebMvcConfiguration {

        @Bean
        @ConditionalOnEnabledResourceChain
        @ConditionalOnMissingFilterBean(ResourceUrlEncodingFilter.class)
        FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() {
            FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>(
                    new ResourceUrlEncodingFilter());
            registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
            return registration;
        }
        //比较重要的视图解析器配置
        @Configuration(proxyBeanMethods = false)
        static class ThymeleafViewResolverConfiguration {

            @Bean
            @ConditionalOnMissingBean(name = "thymeleafViewResolver")
            ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties,
                    SpringTemplateEngine templateEngine) {
                ThymeleafViewResolver resolver = new ThymeleafViewResolver();
                //设置模板引擎
                resolver.setTemplateEngine(templateEngine);
        //字符编码设置
                        resolver.setCharacterEncoding(properties.getEncoding().name());
                resolver.setContentType(
                        appendCharset(properties.getServlet().getContentType(), resolver.getCharacterEncoding()));
                resolver.setProducePartialOutputWhileProcessing(
                        properties.getServlet().isProducePartialOutputWhileProcessing());
                resolver.setExcludedViewNames(properties.getExcludedViewNames());
                resolver.setViewNames(properties.getViewNames());
                // This resolver acts as a fallback resolver (e.g. like a
                // InternalResourceViewResolver) so it needs to have low precedence
                resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
                //Thymeleaf缓存
                resolver.setCache(properties.isCache());
                return resolver;
            }

            private String appendCharset(MimeType type, String charset) {
                if (type.getCharset() != null) {
                    return type.toString();
                }
                LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
                parameters.put("charset", charset);
                parameters.putAll(type.getParameters());
                return new MimeType(type, parameters).toString();
            }

        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.REACTIVE)
    @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
    static class ThymeleafReactiveConfiguration {

        @Bean
        @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
        SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties,
                ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
            SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
            engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
            engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
            templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
            dialects.orderedStream().forEach(engine::addDialect);
            return engine;
        }

    }
    //ThymeleafWebFluxConfiguration自动配置
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.REACTIVE)
    @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
    static class ThymeleafWebFluxConfiguration {

        @Bean
        @ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver")
        ThymeleafReactiveViewResolver thymeleafViewResolver(ISpringWebFluxTemplateEngine templateEngine,
                ThymeleafProperties properties) {
            ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
            resolver.setTemplateEngine(templateEngine);
            mapProperties(properties, resolver);
            mapReactiveProperties(properties.getReactive(), resolver);
            // This resolver acts as a fallback resolver (e.g. like a
            // InternalResourceViewResolver) so it needs to have low precedence
            resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
            return resolver;
        }

        private void mapProperties(ThymeleafProperties properties, ThymeleafReactiveViewResolver resolver) {
            PropertyMapper map = PropertyMapper.get();
            map.from(properties::getEncoding).to(resolver::setDefaultCharset);
            resolver.setExcludedViewNames(properties.getExcludedViewNames());
            resolver.setViewNames(properties.getViewNames());
        }

        private void mapReactiveProperties(Reactive properties, ThymeleafReactiveViewResolver resolver) {
            PropertyMapper map = PropertyMapper.get();
            map.from(properties::getMediaTypes).whenNonNull().to(resolver::setSupportedMediaTypes);
            map.from(properties::getMaxChunkSize).asInt(DataSize::toBytes).when((size) -> size > 0)
                    .to(resolver::setResponseMaxChunkSizeBytes);
            map.from(properties::getFullModeViewNames).to(resolver::setFullModeViewNames);
            map.from(properties::getChunkedModeViewNames).to(resolver::setChunkedModeViewNames);
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(LayoutDialect.class)
    static class ThymeleafWebLayoutConfiguration {

        @Bean
        @ConditionalOnMissingBean
        LayoutDialect layoutDialect() {
            return new LayoutDialect();
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(DataAttributeDialect.class)
    static class DataAttributeDialectConfiguration {

        @Bean
        @ConditionalOnMissingBean
        DataAttributeDialect dialect() {
            return new DataAttributeDialect();
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ SpringSecurityDialect.class })
    static class ThymeleafSecurityDialectConfiguration {

        @Bean
        @ConditionalOnMissingBean
        SpringSecurityDialect securityDialect() {
            return new SpringSecurityDialect();
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Java8TimeDialect.class)
    static class ThymeleafJava8TimeDialect {

        @Bean
        @ConditionalOnMissingBean
        Java8TimeDialect java8TimeDialect() {
            return new Java8TimeDialect();
        }

    }

}

ThymeleafProperties SpringBoot設定クラス属性、ConfigurationPropertiesを使用して属性マッピング注釈です

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

    private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
    //默认的模板资源路径
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    //默认解析html资源
    public static final String DEFAULT_SUFFIX = ".html";

    /**
     * Whether to check that the template exists before rendering it.
     */
    private boolean checkTemplate = true;

    /**
     * Whether to check that the templates location exists.
     */
    private boolean checkTemplateLocation = true;

    /**
     * Prefix that gets prepended to view names when building a URL.
     */
    private String prefix = DEFAULT_PREFIX;

    /**
     * Suffix that gets appended to view names when building a URL.
     */
    private String suffix = DEFAULT_SUFFIX;

    /**
     * Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
     */
     //默认模式也是html的
    private String mode = "HTML";

    /**
     * Template files encoding.
     */
    private Charset encoding = DEFAULT_ENCODING;

    /**
     * Whether to enable template caching.
     */
     //默认开启缓存,项目没上线建议通过配置关闭,然后按F9就可以自动编译,避免影响调试
    private boolean cache = true;

    ....
}

[OK]を、単にViewResolverソースでクリック:ビューのビューを作成するキーコードThymeleafビューリゾルバクラスメソッド、図は、記載VIEWNAMEリダイレクトされる
ここに画像を挿入説明
上記のプロセスからは、前方に見または方法と同様にリダイレクトすることができます調整は、RedirectViewクラスを参照するリダイレクト次のようにキーコードを見つけるために、ソースをオンにする。
ここに画像を挿入説明
また、このクラスでは、ステータスコードは、最初の要求セットを行う、次いでresponse.sendRedirect(encodedURL);
ここに画像を挿入説明
及び前方によって図方法は、ページに移動します:
ここに画像を挿入説明

付録:
公式の例Thymeleaf

おすすめ

転載: www.cnblogs.com/mzq123/p/11965478.html