从数据库加载FreeMarker模板

28

我希望将我的 FreeMarker 模板存储在一个类似于以下的数据库表中:

template_name | template_content
---------------------------------
hello         |Hello ${user}
goodbye       |So long ${user}

当收到特定名称的模板请求时,应该执行查询以加载相关的模板内容。这个模板内容连同数据模型(上面示例中'user'变量的值)应该传递给FreeMarker。
但是,FreeMarker API似乎假定每个模板名称对应于文件系统中特定目录中同名的文件。是否有任何简单的方法可以让我的模板从数据库中加载而不是从文件系统中加载?
编辑:我应该提到,我希望能够在应用程序运行时将模板添加到数据库中,因此我不能简单地在启动时将所有模板加载到新的StringTemplateLoader中(如下所建议)。
6个回答

36

我们使用StringTemplateLoader来加载从数据库获取的模板(正如Dan Vinton所建议的那样)。

这里是一个例子:

StringTemplateLoader stringLoader = new StringTemplateLoader();
String firstTemplate = "firstTemplate";
stringLoader.putTemplate(firstTemplate, freemarkerTemplate);
// It's possible to add more than one template (they might include each other)
// String secondTemplate = "<#include \"greetTemplate\"><@greet/> World!";
// stringLoader.putTemplate("greetTemplate", secondTemplate);
Configuration cfg = new Configuration();
cfg.setTemplateLoader(stringLoader);
Template template = cfg.getTemplate(firstTemplate);

编辑 您不必在启动时加载所有模板。每当我们访问模板时,我们将通过从DB中检索它并通过StringLoader进行加载,并调用template.process()来生成(在我们的情况下)XML输出。


18
有几种方法:
  • 创建一个新的 TemplateLoader 实现,从数据库直接加载模板,并在加载任何模板之前使用 setTemplateLoader() 将其传递给您的 Configuration 实例。

  • 在应用程序启动时使用从数据库配置的 StringTemplateLoader。将其添加到上述配置中。

根据问题者的编辑,你可以编辑自己的 TemplateLoader 实现。请参考 Javadoc 这里,这是一个只有四个方法的简单接口,行为已经很好地记录下来了。


7

4

如果你正在寻找一些代码,这里有一个例子。查看代码中的注释以获得更好的理解。

DBTemplate:

@Entity
public class DBTemplate implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    private long templateId;

    private String content; // Here's where the we store the template

    private LocalDateTime modifiedOn;

}

模板加载器实现(EMF是一个EntityManagerFactory实例):

public class TemplateLoaderImpl implements TemplateLoader {

    public TemplateLoaderImpl() { }

    /**
     * Retrieves the associated template for a given id.
     *
     * When Freemarker calls this function it appends a locale
     * trying to find a specific version of a file. For example,
     * if we need to retrieve the layout with id = 1, then freemarker
     * will first try to load layoutId = 1_en_US, followed by 1_en and
     * finally layoutId = 1.
     * That's the reason why we have to catch NumberFormatException
     * even if it is comes from a numeric field in the database.
     *
     * @param layoutId
     * @return a template instance or null if not found.
     * @throws IOException if a severe error happens, like not being
     * able to access the database.
     */
    @Override
    public Object findTemplateSource(String templateId) throws IOException {

        EntityManager em = null;

        try {
            long id = Long.parseLong(templateId);
            em = EMF.getInstance().getEntityManager();
            DBTemplateService service = new DBTemplateService(em);
            Optional<DBTemplate> result = service.find(id);
            if (result.isPresent()) {
                return result.get();
            } else {
                return null;
            }
        } catch (NumberFormatException e) {
            return null;
        } catch (Exception e) {
            throw new IOException(e);
        } finally {
            if (em != null && em.isOpen()) {
                em.close();
            }
        }
    }


    /**
     * Returns the last modification date of a given template.
     * If the item does not exist any more in the database, this
     * method will return Long's MAX_VALUE to avoid freemarker's
     * from recompiling the one in its cache.
     *
     * @param templateSource
     * @return
     */
    @Override
    public long getLastModified(Object templateSource) {
        EntityManager em = null;
        try {
            em = EMF.getInstance().getEntityManager();
            DBTemplateService service = new DBTemplateService(em);
            // Optimize to only retrieve the date
            Optional<DBTemplate> result = service.find(((DBTemplate) templateSource).getTemplateId());
            if (result.isPresent()) {
                return result.get().getModifiedOn().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            } else {
                return Long.MAX_VALUE;
            }
        } finally {
            if (em != null && em.isOpen()) {
                em.close();
            }
        }
    }

    /**
     * Returns a Reader from a template living in Freemarker's cache.
     */
    @Override
    public Reader getReader(Object templateSource, String encoding) throws IOException {
        return new StringReader(((DBTemplate) templateSource).getContent());
    }

    @Override
    public void closeTemplateSource(Object templateSource) throws IOException {
        // Nothing to do here...
    }

}

设置配置类:

...
TemplateLoaderImpl loader = new TemplateLoaderImpl();

templateConfig = new Configuration(Configuration.VERSION_2_3_25);
templateConfig.setTemplateLoader(loader);
...

最后,使用它:
...
long someId = 3L;
Template template = templateConfig.getTemplate("" + someId);
...

这很好用,可以让你使用Freemarker的所有功能,如导入、包含等。请参考以下示例:
<#import "1" as layout> <!-- Use a template id. -->
<@layout.mainLayout>
...

或者在:

<#include "3"> <!-- Use a template id. -->
...

我在我的 CMS(CinnamonFramework)上使用这个加载器,效果非常好。

最好的祝福,


1
我认为你可以设置 Configuration.setLocalizedLookup(boolean) 来禁用本地化查找,这样你就不必捕获 NumberFormatException。 - Artur

3

虽然这是一个老问题,但对于任何遇到相同问题的人,我找到了一种简单的解决方案,无需自定义模板加载器或在启动时加载模板。

假设您的数据库中有动态模板:

数据库:

<p>Hello <b>${params.user}</b>!</p>

您可以创建一个Freemarker文件(ftlh),解释接收到的字符串(content),并使用interpret生成模板:

dynamic.ftlh:

<#assign inlineTemplate = content?interpret>
<@inlineTemplate />

然后在您的Java代码中,您只需要从数据库获取字符串(就像从数据库检索任何其他数据一样),并使用具有interpret的文件生成模板:

Java:

String content = getFromDatabase(); 
Configuration cfg = getConfiguration(); 
String filePath = "dynamic.ftlh";

Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "World");

Map<String, Object> root = new HashMap<>();
root.put("content", content);   
root.put("params", params);     

Template template = cfg.getTemplate(filePath);

try (Writer out = new StringWriter()) {
    template.process(root, out);
    String result = out.toString();
    System.out.println(result);
}

(将getFromDatabase()getConfiguration()方法更改为您想要从数据库获取动态内容的任何内容以及相应地获取Freemarker配置对象

这将打印:

<p>Hello <b>World</b>!</p>

然后,您可以在数据库中更改动态内容或创建其他内容,添加新参数等,而无需创建其他Freemarker文件(ftlh)。


interpret内置功能正是我一直在寻找的特性。相比其他解决方案或绕过方法,如先手动处理内容,然后将其作为变量添加到最终模板中,这使得工作更加轻松。 - djmj

0

实现配置。

示例:

@Configuraton
public class FreemarkerConfig {

@Autowired
TemplateRepository tempRepo;

@Autowired
TemplateUtils tempUtils;

@Primary
@Bean   
public FreeMarkerConfigurationFactoryBean getFreeMarkerConfiguration() {
    // Create new configuration bean
    FreeMarkerConfigurationFactoryBean bean = new FreeMarkerConfigurationFactoryBean();
    // Create template loader
    StringTemplateLoader sTempLoader = new StringTemplateLoader();
    // Find all templates
    Iterable<TemplateDb> ite = tempRepo.findAll();
    ite.forEach((template) -> {
        // Put them in loader
        sTempLoader.putTemplate(template.getFilename(), template.getContent()); 
    });
    // Set loader
    bean.setPreTemplateLoaders(sTempLoader);
    return bean;
}

}

然后你可以像这样使用它:

@Autowired
private Configuration freemarkerConfig;

  Template template = freemarkerConfig.getTemplate(templateFilePath);
  String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, mapTemplate);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接