利用thymeleaf双层模板技术,制作个性化报表

需求背景

通常我们会碰到一些带表格的报表需求,一般是包含一些表格和一些循环列表的需求,每个表格的样式可以不一样。
有些公司会使用一些专业的报表软件,例如jasperReport,如意报表等,虽然这种软件功能很强大,但是需要一定的学习门槛,而且使用中调整样式并没有想象中方便,最重要的是此类模板设计、模板导出、模板上传都是跟业务系统分开的,不能嵌入系统中使用,不能做到在线编辑,在线预览。
为此,我专门研究了thymeleaf来完成此类需求的办法,希望做到报表上的所有表格都可以通过配置配出来,支持纯文本和动态变量,支持图片,所有表格的样式可以通过css来随意配置,支持循环列表,支持横向和纵向合并单元格
为什么选择thymeleaf而不是选择freemark等其他模板技术,主要基于两个原因:
1、spring官方推荐用thymeleaf,并且springboot有官方适配
2、thymeleaf编写的模板能直接打开预览,不会像freemark破坏了html原本的格式。

实现思路

核心思路是:设计一个根模板(root-template.html),它可以通过设计好的数据库配置生成新的具体模板(biz-template.html),并且biz-template.html里面包含具体的业务数据的变量,这样就可以结合业务数据展示出报表。这就是我称之为双层模板的原因。
技术细节包含以下几点:
1、根模板(root-template.html)如何设计
2、需要用thymeleaf解析html并生成html,就必须使用API的方式去实现,且html的路径可配置,否则线上环境无法生成html到jar包里
3、生成后的html也需要包含thymeleaf的标签,那就需要研究thymeleaf如何生成带标签的模板
4、样式和合并单元格如何控制

根模板设计

根页面定好10个table区域,每个区域都是一样的逻辑,判断table配置的数据集是否为空,为空则整个table不显示,不为空,则先循环table配置的tr信息,tr里面再循环配置的td信息。
根模板如下:


image.png

springboot整合thymeleaf,并支持api的方式生成html

1、pom.xml

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
        <relativePath/>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
    </dependencies>

2、application.properties

server.port=8080

spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.check-template-location=true
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
spring.thymeleaf.mode=HTML5

report.templates.path=

3、ThymeleafConfig配置类

@Configuration
@EnableWebMvc
public class ThymeleafConfig extends WebMvcAutoConfiguration {
    @Autowired
    ApplicationContext applicationContext;

    /**
     * eg: E:/data/resources/templates/
     */
    @Value("${report.templates.path}")
    private String reportTemplatesPath;

    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine);
        viewResolver.setCache(false);
        return viewResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
//        engine.setEnableSpringELCompiler(true);
        engine.setTemplateResolver(templateResolver());
        return engine;
    }

    @Bean
    public ITemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        URL resource = this.getClass().getClassLoader().getResource("templates/");       //这里把系统获取到的Class的path替换为源码对应的Path,这样修改的时候就可以动态刷新
        String devResource = resource.getFile().toString().replaceAll("target/classes", "src/main/resources");
        if (reportTemplatesPath!=null&&!"".equals(reportTemplatesPath.trim())){
            devResource=reportTemplatesPath.trim();
        }
        resolver.setPrefix("file://"+devResource);
        resolver.setCacheable(false);//不允许缓存
        resolver.setSuffix(".html");
        return resolver;
    }
}

这样就可以使用以下代码来生成html文件了

    @Autowired
    private TemplateEngine templateEngine;

        Context context = new Context();
        context.setVariable("table1", templateData.getTable1());
        FileWriter write = new FileWriter(devResource+"/biz-template.html");
        templateEngine.process("root-template", context, write);

利用thymeleaf生成带thymeleaf标签的html文件

主要利用两点,

  • 一个是th:attr

例如,根模板里面:

<div th:attr="'th:text'=${td.value}"></div>

如果td.value=${userName},则通过根模板生成后的子模板html代码是:

<div th:text="${userName}"></div>

看到没,子模板就也可以再用thymeleaf进行解析

这里有两点特别说明:
1、<div th:attr="'th:attr'=${td.value}"></div> 这种方式行不通,通过th:attr无法增加th:attr标签
2、<div th:attr="'th:text'=${td.value}" th:text="${td.value}"></div> 这种方式也不行,只能解析后面那个th:text,解析结果如下:
<div>${userName}</div>
那如果又想加th:text标签又想设置text内容怎么办呢?答案是增加一层div:
<div th:attr="'th:text'=${td.value}" ><div th:text="${td.value}"></div></div>

  • 一个是th:utext

例如,根模板里面:

<div th:utext="${td.value}"></div>

如果td.value是以下字符串<img th:src="${imgUrl}">,则通过根模板生成后的子模板html代码是:

<div><img th:src="${imgUrl}"></div>

不错吧,这样也可以再用thymeleaf解析

样式和合并单元格问题

使用th:style,th:class,th:rowspan,th:clospan来控制
根模板中要设置全局自定义样式变量可以使用如下配置:

<style th:inline="text">
    [[${style}]]
</style>

效果演示

子模板template2


image.png

子模板2填充数据后:


image.png

子模板template3
image.png

子模板3填充数据后:


image.png

最终的html如果需要转成pdf,请参考最好的html转pdf工具(wkhtmltopdf)

常用语法

注释

1、静态文件打开可以看到,但是thymeleaf生成后看不到

<!--/*--> 
  <div>
     you can see me only before Thymeleaf processes me!
  </div>
<!--*/-->

2、静态文件打开看不到,但是thymeleaf生成后看得到

<span>hello!</span>
<!--/*/
  <div th:text="${...}">
    ...
  </div>
/*/-->
<span>goodbye!</span>

3、块代码段,th:block,可以配合thymeleaf的其他标签去定义代码块,例如th:each如果循环单行<tr>的时候,th:each可以放在<tr>标签里,但是如果要循环多行<tr>的话,则可以配合th:block来使用,如下:

<table>
  <th:block th:each="user : ${users}">
    <tr>
        <td th:text="${user.login}">...</td>
        <td th:text="${user.name}">...</td>
    </tr>
    <tr>
        <td colspan="2" th:text="${user.address}">...</td>
    </tr>
  </th:block>
</table>

删除代码段

以下是Thymeleaf的一个例子。我们可以使用th:remove来删除指定的部分,这在原型设计和调试的时候很有用。

  <tr th:remove="all">
    <td>Mild Cinnamon</td>
    <td>1.99</td>
    <td>yes</td>
    <td>
      <span>3</span> comment/s
      <a href="comments.html">view</a>
    </td>
  </tr>

th:remove可接受的值有5个:

  • all: 移除标签和所有子元素
  • body: 移除所有子元素,保留标签
  • tag: 移除标签,保留子元素
  • all-but-first: 保留第一个子元素,移除所有其他
  • none : 什么也不做。这个值在动态求值的时候会有作用

特别注意点

Thymeleaf中无法利用map.key表达式获取到map的键值,特别是使用 th:if="map.key =null" 的时候,如果map里面不包含key,则立马报错,所以如果要用从map里面取某个key,则map里面必须要有这个key值(value为空也必须设置为null),否则会报错

参考

Springboot 之 引入Thymeleaf
thymeleaf 基本语法
spring boot(四):thymeleaf使用详解
Spring Web MVC框架(十二) 使用Thymeleaf
使用Thymeleaf API渲染模板生成静态页面
Spring Boot中Thymeleaf引擎动态刷新
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容