使用来自数据库的 Qute 模板
简介
我是 Red Hat 团队的一员,该团队创建了一个多租户通知服务,用于发送来自多个 Red Hat Hybrid Cloud Console 应用程序(即租户)的通知。我们的服务可用于发送各种通知,包括电子邮件。每个租户都可以创建任意数量的电子邮件模板,并将其与触发通知的事件关联起来。
我们使用强大的 Qute 模板引擎 和存储在 src/main/resources/templates
文件夹中的模板来实现这一点。这使得我们的租户能够以最少的 Qute 知识来设计适合他们需求的模板。然而,我们很快意识到,编辑模板对租户来说是一个缓慢且繁重的过程。事实上,他们必须在我们的存储库中创建一个 GitHub 拉取请求,等待审核,然后再等待部署,之后才能测试模板。我们需要让租户更轻松地完成这个过程,最好是自助服务。
然后,我们决定使用 Qute 的 TemplateLocator
将模板从文件存储移动到数据库。这有助于我们为租户提供一种更轻松、无障碍且自助的方式来编辑模板。
我们是这样做的。
显而易见的部分:将模板持久化到数据库
在将模板用于 Qute 之前,显然需要将模板持久化。这如何执行并不重要。任何形式的 Hibernate(响应式或非响应式,带 Panache 或不带 Panache)都可以正常工作。本文将展示基于 Hibernate with Panache 的示例。
接下来的章节将使用此 JPA 实体
package org.acme;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class DbTemplate extends PanacheEntityBase {
@Id
public String name; (1)
public String content;
}
1 | 模板名称将是数据库主键。 |
有趣的部分:将 Qute 连接到数据库
现在模板可以持久化了,我们需要一种方法从 Qute 中使用它们。幸运的是,Qute 提供了一个非常有趣的接口,称为 TemplateLocator
,可用于从任何位置加载模板,包括从数据库加载。
以下是如何将其与我们之前定义的 DbTemplate
实体一起使用
package org.acme;
import io.quarkus.logging.Log;
import io.quarkus.qute.EngineBuilder;
import io.quarkus.qute.TemplateLocator;
import io.quarkus.qute.Variant;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import java.io.Reader;
import java.io.StringReader;
import java.util.Optional;
@ApplicationScoped
public class DbTemplateLocator implements TemplateLocator {
@Override
public Optional<TemplateLocation> locate(String name) {
DbTemplate template = DbTemplate.findById(name);
if (template == null) {
Log.tracef("Template with [name=%s] not found in the database", name);
return Optional.empty();
} else {
Log.tracef("Template with [name=%s] found in the database", name);
return Optional.of(buildTemplateLocation(template.getContent()));
}
}
@Override
public int getPriority() { (1)
return DEFAULT_PRIORITY - 1;
}
void configureEngine(@Observes EngineBuilder builder) { (2)
builder.addLocator(this);
}
private TemplateLocation buildTemplateLocation(String templateContent) {
return new TemplateLocation() {
@Override
public Reader read() {
return new StringReader(templateContent);
}
@Override
public Optional<Variant> getVariant() {
return Optional.empty();
}
};
}
}
1 | 如果您的 Quarkus 应用程序包含从文件系统和数据库加载的模板,您将需要覆盖模板定位器的默认优先级。否则,Quarkus 将尝试使用 DbTemplateLocator 从文件系统加载模板,这可能导致异常或不可预测的行为。 |
2 | 在 Quarkus 2.10 之前,将 DbTemplateLocator 与 Quarkus 提供的 Qute 引擎实例集成只能通过 CDI 观察者完成,如下所示。 |
Quarkus 2.10 最近引入了一个新的 |
现在模板定位器已注册,我们可以使用 Qute 编译和渲染数据库中的模板。正如您在以下示例中所见,数据库模板的使用方式与文件模板完全相同
package org.acme;
import io.quarkus.qute.Engine;
import io.quarkus.qute.Template;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
@ApplicationScoped
public class EmailSender {
@Inject
Engine engine;
public void sendEmail(String templateName) {
Template template = engine.getTemplate(templateName);
if (template != null) {
String rendered = template.render();
// Send an email using the template.
}
}
}
注意 Qute 的内部缓存
每当 Qute 加载一个模板时,它都会存储在一个内部的 ConcurrentHashMap
中,并永远保留在内存中,除非 Qute 被指示另行处理。这意味着您需要在使用模板更新或删除数据库中的模板后,将其从 Qute 内部缓存中删除。
有几种方法可以实现这一点
package org.acme;
import io.quarkus.qute.Engine;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
@ApplicationScoped
public class DbEngineCacheManager {
@Inject
Engine engine;
public void removeTemplates(String name) {
engine.removeTemplates(templateName -> templateName.equals(name)); (1)
}
public void clearAll() {
engine.clearTemplates(); (2)
}
}
1 | 这会删除映射 ID 与给定谓词匹配的模板。 |
2 | 这会从缓存中删除所有模板。 |
清除此内部缓存可能会变得棘手,特别是当您的应用程序运行在具有多个副本的 Kubernetes 集群中时。您确实需要一种方法将指令广播到所有 Pod(可能使用 Kafka 主题或数据库表),以从缓存中删除已更新或删除的模板。有一种更便宜(但很不完美)的方法可以保持所有 Pod 缓存同步,即使用计划任务
package org.acme;
import io.quarkus.qute.Engine;
import io.quarkus.scheduler.Scheduled;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
@ApplicationScoped
public class DbEngineCacheScheduledCleaner {
@Inject
Engine engine;
@Scheduled(every = "5m", delayed = "5m") (1)
public void clearTemplates() {
engine.clearTemplates();
}
}
1 | 所有模板将在每 5 分钟后从内部缓存中清除。 |
防止删除包含的模板
Qute 模板可以包含在另一个模板中。如果内部模板被删除,那么外部模板的编译将失败,而这显然是在从数据库加载模板时需要避免的。
以下是在删除模板之前查找其是否被包含在另一个模板中的方法
package org.acme;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
@ApplicationScoped
public class TemplateRepository {
@Inject
EntityManager entityManager;
@Transactional
public void deleteTemplate(String name) {
long count = entityManager.createQuery("SELECT COUNT(*) FROM DbTemplate WHERE name != :name AND content LIKE :include", Long.class)
.setParameter("name", name)
.setParameter("include", "%{#include " + name + "%")
.getSingleResult();
if (count > 0) {
throw new IllegalStateException("Included templates can't be deleted, remove the inclusion or delete the outer template first");
} else {
entityManager.createQuery("DELETE FROM DbTemplate WHERE name = :name")
.setParameter("name", name)
.executeUpdate();
}
}
}
数据库模板验证
数据库模板有一个明显的缺点:Quarkus 不再能够执行 类型安全 验证。
语法验证也从构建时推迟到运行时,但这是意料之中的,因为模板可以在运行时创建或编辑。