在后端开发中,我们经常需要根据业务数据动态生成各种文本内容:从配置文件到源代码,从SQL脚本到部署文档。
这些重复性的生成任务不仅耗时耗力,还容易出错。模板引擎为我们提供了一种有效的解决方案。
后端开发的重复性工作痛点
在后端开发过程中,经常存在大量重复性的工作需要完成
1. 配置文件需要根据环境动态生成
- 多环境配置管理:需要根据数据库类型、环境参数等信息生成不同的application.yml配置文件
- 基础设施配置:Nginx、Docker、Kubernetes等配置文件需要根据服务信息动态生成
- 配置模板复用:不同项目的配置结构相似,但参数不同,需要统一的配置模板
2. 代码结构高度相似需要模板化生成
- CRUD代码生成:根据数据库表结构自动生成Entity、Repository、Service、Controller等代码
- API接口文档生成:基于Controller注解自动生成接口文档,保持文档与代码同步
- 项目脚手架:创建新项目时快速生成标准化的基础代码结构
3. 文档和脚本需要标准化模板
- 数据库设计文档:根据表结构信息自动生成数据库设计文档
- 部署脚本生成:根据项目配置自动生成部署脚本和说明文档
- API文档同步:基于代码中的注解和结构生成最新的API文档
4. 通知和报表需要动态内容生成
- 邮件内容模板化:用户注册、密码重置、订单通知等邮件内容需要根据用户数据动态生成
- 日志格式标准化:统一的日志格式模板,包含时间戳、请求ID、用户信息等字段
解决方案:基于模板引擎的自动化生成
模板引擎通过将模板与数据分离,实现了文本内容的自动化生成。核心思想是:
- 模板定义:创建包含占位符和控制逻辑的模板文件
- 数据准备:构建包含生成所需数据的数据模型
- 引擎处理:模板引擎将数据与模板结合,生成最终的文本内容
工作原理详解
模板引擎的工作流程可以分为以下几个步骤:
- 1. 模板解析:引擎解析模板文件,构建抽象语法树(AST)
- 2. 数据绑定:将传入的数据对象与模板中的变量进行绑定
- 3. 表达式求值:计算模板中的表达式,生成动态内容
- 4. 内容生成:将处理后的内容输出为目标格式
技术选型
在后端场景中,常用的模板引擎有FreeMarker、Velocity和Thymeleaf等。
Spring Boot集成FreeMarker:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
基础配置示例:
yaml
spring:
freemarker:
template-loader-path: classpath:/templates/
suffix: .ftl
charset: UTF-8
content-type: text/plain
cache: true # 生产环境建议开启缓存
settings:
template_update_delay_milliseconds: 0
default_encoding: UTF-8
output_encoding: UTF-8
locale: zh_CN
datetime_format: yyyy-MM-dd HH:mm:ss
number_format: 0.##
boolean_format: true,false
模板引擎的优势
相比传统的字符串拼接或硬编码方式,模板引擎具有明显优势:
- 1. 可维护性强:模板文件与业务代码分离,修改模板不需要重新编译
- 2. 可读性好:模板语法接近自然语言,易于理解和维护
- 3. 功能丰富:内置条件判断、循环、函数调用等强大功能
- 4. 缓存机制:支持模板缓存,提高生成性能
- 5. 错误处理:提供详细的错误信息,便于调试和问题定位
配置文件生成实战
1. Nginx配置生成器
模板文件 templates/config/nginx.conf.ftl
nginx
# Nginx Configuration for ${serviceName}
# Generated at: .now
upstream ${serviceName}_backend {
#least_conn;
<#list servers as server>
server ${server.host}:${server.port}<#if server.weight??> weight=${server.weight}</#if>;
</#list>
}
server {
listen ${port};
server_name ${domain};
access_log /var/log/nginx/${serviceName}_access.log;
error_log /var/log/nginx/${serviceName}_error.log;
<#if sslEnabled>
ssl_certificate ${sslCertificatePath};
ssl_certificate_key ${sslCertificateKeyPath};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
</#if>
location / {
proxy_pass http://${serviceName}_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
<#if timeout??>
proxy_connect_timeout ${timeout};
proxy_send_timeout ${timeout};
proxy_read_timeout ${timeout};
</#if>
}
<#if staticPath??>
location /static/ {
alias ${staticPath}/;
expires 1d;
add_header Cache-Control "public, immutable";
}
</#if>
<#if healthCheckPath??>
location ${healthCheckPath} {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
</#if>
}
配置生成服务
java
@Service
public class ConfigGeneratorService {
@Autowired
private Configuration freemarkerConfig;
public String generateNginxConfig(NginxConfig config) throws IOException, TemplateException {
Template template = freemarkerConfig.getTemplate("config/nginx.conf.ftl");
// 添加当前时间戳
config.setNow(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
try(StringWriter out = new StringWriter()) {
template.process(config, out);
return out.toString();
}
}
public void saveConfigToFile(String configContent, String outputPath) throws IOException {
Path path = Paths.get(outputPath);
Files.createDirectories(path.getParent());
Files.write(path, configContent.getBytes(StandardCharsets.UTF_8));
}
}
// 配置数据模型
@Data
public class NginxConfig {
private String serviceName;
private String domain;
private int port;
private boolean sslEnabled;
private String sslCertificatePath;
private String sslCertificateKeyPath;
private String staticPath;
private String healthCheckPath;
private Integer timeout;
private List<ServerInfo> servers = new ArrayList<>();
@Data
public static class ServerInfo {
private String host;
private int port;
private Integer weight;
}
}
使用示例
java
@RestController
@RequestMapping("/api/config")
public class ConfigController {
@Autowired
private ConfigGeneratorService configGenerator;
@PostMapping("/nginx")
public ResponseEntity<String> generateNginxConfig(@RequestBody NginxConfig config) {
try {
String configContent = configGenerator.generateNginxConfig(config);
// 保存到文件
String outputPath = "/etc/nginx/sites-available/" + config.getServiceName() + ".conf";
configGenerator.saveConfigToFile(configContent, outputPath);
return ResponseEntity.ok(configContent);
} catch (Exception e) {
return ResponseEntity.status(500).body("配置生成失败: " + e.getMessage());
}
}
}
2. Docker Compose配置生成
模板文件 templates/config/docker-compose.yml.ftl
yaml
version: '3.8'
services:
<#list services as service>
${service.name}:
image: ${service.image}:${service.tag}
container_name: ${service.containerName}
<#if service.ports??>
ports:
<#list service.ports as port>
- "${port.host}:${port.container}"
</#list>
</#if>
<#if service.environment??>
environment:
<#list service.environment as key, value>
${key}: "${value}"
</#list>
</#if>
<#if service.volumes??>
volumes:
<#list service.volumes as volume>
- ${volume.host}:${volume.container}
</#list>
</#if>
<#if service.dependsOn??>
depends_on:
<#list service.dependsOn as dependency>
- ${dependency}
</#list>
</#if>
<#if service.networks??>
networks:
<#list service.networks as network>
- ${network}
</#list>
</#if>
restart: unless-stopped
</#list>
<#if networks??>
networks:
<#list networks as network>
${network.name}:
driver: ${network.driver!"bridge"}
</#list>
</#if>
<#if volumes??>
volumes:
<#list volumes as volume>
${volume.name}:
</#list>
</#if>
代码生成实战
1. CRUD代码生成器
实体类模板 templates/code/Entity.java.ftl
java
package ${packageName}.entity;
<#list imports as import>
import ${import};
</#list>
/**
* ${tableName} 实体类
*
* @author ${author}
* @since ${date}
*/
@Data
<#if tableName??>
@TableName("${tableName}")
</#if>
public class ${className} implements Serializable {
private static final long serialVersionUID = 1L;
<#list fields as field>
/** ${field.comment!} */
<#if field.id>
@TableId(type = IdType.${field.idType!"AUTO"})
</#if>
<#if field.notNull>
@NotNull(message = "${field.comment!}不能为空")
</#if>
<#if field.length??>
@Size(max = ${field.length}, message = "${field.comment!}长度不能超过${field.length}")
</#if>
private ${field.type} ${field.name};
</#list>
<#-- Getter和Setter方法由Lombok @Data注解自动生成 -->
}
Service接口模板 templates/code/Service.java.ftl
java
package ${packageName}.service;
import ${packageName}.entity.${className};
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
/**
* ${className} 服务接口
*
* @author ${author}
* @since ${date}
*/
public interface ${className}Service {
/**
* 分页查询${className}
*/
Page<${className}> select${className}Page(int page, int size, ${className} ${classNameInstance});
/**
* 根据ID查询${className}
*/
${className} select${className}ById(${primaryKey.type} ${primaryKey.name});
/**
* 新增${className}
*/
int insert${className}(${className} ${classNameInstance});
/**
* 修改${className}
*/
int update${className}(${className} ${classNameInstance});
/**
* 批量删除${className}
*/
int delete${className}ByIds(List<${primaryKey.type}> ids);
/**
* 删除${className}
*/
int delete${className}ById(${primaryKey.type} ${primaryKey.name});
}
Controller模板 templates/code/Controller.java.ftl
java
package ${packageName}.controller;
import ${packageName}.entity.${className};
import ${packageName}.service.${className}Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* ${className} 控制器
*
* @author ${author}
* @since ${date}
*/
@Api(tags = "${className}管理")
@RestController
@RequestMapping("/api/${classNameInstance}")
public class ${className}Controller {
@Autowired
private ${className}Service ${classNameInstance}Service;
@ApiOperation("分页查询${className}")
@GetMapping("/page")
public Page<${className}> get${className}Page(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
${className} ${classNameInstance}) {
return ${classNameInstance}Service.select${className}Page(page, size, ${classNameInstance});
}
@ApiOperation("根据ID查询${className}")
@GetMapping("/{id}")
public ${className} get${className}ById(@PathVariable ${primaryKey.type} id) {
return ${classNameInstance}Service.select${className}ById(id);
}
@ApiOperation("新增${className}")
@PostMapping
public int create${className}(@RequestBody ${className} ${classNameInstance}) {
return ${classNameInstance}Service.insert${className}(${classNameInstance});
}
@ApiOperation("修改${className}")
@PutMapping("/{id}")
public int update${className}(@PathVariable ${primaryKey.type} id, @RequestBody ${className} ${classNameInstance}) {
${classNameInstance}.set${primaryKey.name?cap_first}(id);
return ${classNameInstance}Service.update${className}(${classNameInstance});
}
@ApiOperation("删除${className}")
@DeleteMapping("/{id}")
public int delete${className}(@PathVariable ${primaryKey.type} id) {
return ${classNameInstance}Service.delete${className}ById(id);
}
}
代码生成服务
java
@Service
public class CodeGeneratorService {
@Autowired
private Configuration freemarkerConfig;
@Value("${code.output.path}")
private String outputBasePath;
public void generateCode(GenerateRequest request) throws Exception {
Map<String, Object> dataModel = buildDataModel(request);
// 生成实体类
generateFile("code/Entity.java.ftl",
getEntityPath(request.getPackageName(), request.getClassName()),
dataModel);
// 生成Service接口
generateFile("code/Service.java.ftl",
getServicePath(request.getPackageName(), request.getClassName()),
dataModel);
// 生成Controller
generateFile("code/Controller.java.ftl",
getControllerPath(request.getPackageName(), request.getClassName()),
dataModel);
}
private Map<String, Object> buildDataModel(GenerateRequest request) {
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("packageName", request.getPackageName());
dataModel.put("className", request.getClassName());
dataModel.put("classNameInstance", StringUtils.uncapitalize(request.getClassName()));
dataModel.put("tableName", request.getTableName());
dataModel.put("author", request.getAuthor());
dataModel.put("date", LocalDate.now().toString());
dataModel.put("fields", request.getFields());
dataModel.put("primaryKey", findPrimaryKey(request.getFields()));
dataModel.put("imports", calculateImports(request.getFields()));
return dataModel;
}
private void generateFile(String templateName, String outputPath, Map<String, Object> dataModel)
throws IOException, TemplateException {
Template template = freemarkerConfig.getTemplate(templateName);
Path path = Paths.get(outputBasePath, outputPath);
Files.createDirectories(path.getParent());
try(StringWriter out = new StringWriter()) {
template.process(dataModel, out);
Files.write(path, out.toString().getBytes(StandardCharsets.UTF_8));
}
}
// 工具方法
private String getEntityPath(String packageName, String className) {
return packageName.replace('.', '/') + "/entity/" + className + ".java";
}
private String getServicePath(String packageName, String className) {
return packageName.replace('.', '/') + "/service/" + className + "Service.java";
}
private String getControllerPath(String packageName, String className) {
return packageName.replace('.', '/') + "/controller/" + className + "Controller.java";
}
}
邮件模板处理
1. HTML邮件模板
模板文件 templates/email/welcome.html.ftl
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>欢迎加入${companyName}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #007bff; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; }
.footer { background-color: #f8f9fa; padding: 10px; text-align: center; font-size: 12px; }
.button {
display: inline-block;
padding: 12px 24px;
background-color: #28a745;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>欢迎加入${companyName}!</h1>
</div>
<div class="content">
<h2>亲爱的${user.name},</h2>
<p>感谢您注册${companyName}!您的账户已经创建成功。</p>
<p>您的账户信息:</p>
<ul>
<li>用户名:${user.username}</li>
<li>邮箱:${user.email}</li>
<#if user.phone??>
<li>手机号:${user.phone}</li>
</#if>
</ul>
<p>请点击下面的按钮激活您的账户:</p>
<div style="text-align: center;">
<a href="${activationUrl}" class="button">激活账户</a>
</div>
<#if activationToken??>
<p>如果按钮无法点击,请复制以下链接到浏览器:</p>
<p style="word-break: break-all; background-color: #f8f9fa; padding: 10px;">
${activationUrl}?token=${activationToken}
</p>
</#if>
<p>激活链接将在${expiryHours}小时后失效,请尽快完成激活。</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿回复。</p>
<p>© ${currentYear} ${companyName}. 保留所有权利。</p>
<#if unsubscribeUrl??>
<p><a href="${unsubscribeUrl}">取消订阅</a></p>
</#if>
</div>
</div>
</body>
</html>
邮件服务
java
@Service
public class EmailService {
@Autowired
private Configuration freemarkerConfig;
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.from}")
private String fromEmail;
public void sendWelcomeEmail(User user, String activationToken) throws Exception {
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("user", user);
dataModel.put("companyName", "我的公司");
dataModel.put("activationUrl", "https://myapp.com/activate");
dataModel.put("activationToken", activationToken);
dataModel.put("expiryHours", 24);
dataModel.put("currentYear", Year.now().getValue());
Template template = freemarkerConfig.getTemplate("email/welcome.html.ftl");
try(StringWriter out = new StringWriter()) {
template.process(dataModel, out);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(fromEmail);
helper.setTo(user.getEmail());
helper.setSubject("欢迎加入" + dataModel.get("companyName"));
helper.setText(out.toString(), true); // true表示HTML格式
mailSender.send(message);
}
}
}
高级技巧与最佳实践
1. 模板继承与片段复用
基础配置模板 templates/config/base.conf.ftl
conf
# 基础配置文件
# 生成时间: ${generationTime}
# 环境: ${environment}
<#macro logConfig>
# 日志配置
logging.level.root=${logLevel!"INFO"}
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
</#macro>
<#macro databaseConfig>
# 数据库配置
spring.datasource.url=${database.url}
spring.datasource.username=${database.username}
spring.datasource.password=${database.password}
spring.datasource.driver-class-name=${database.driver!"com.mysql.cj.jdbc.Driver"}
</#macro>
<#macro serverConfig>
# 服务器配置
server.port=${server.port!8080}
server.servlet.context-path=${server.contextPath!"/"}
</#macro>
具体配置模板 templates/config/application.yml.ftl
yaml
<#include "base.conf.ftl">
# Spring Boot Application Configuration
<@serverConfig/>
<@databaseConfig/>
<@logConfig/>
# 应用特定配置
app:
name: ${appName}
version: ${appVersion!"1.0.0"}
<#if customConfig??>
<#list customConfig as key, value>
${key}: ${value}
</#list>
</#if>
<#if profiles??>
spring:
profiles:
active: ${profiles.active!"dev"}
</#if>
2. 模板缓存与性能优化
java
@Configuration
public class TemplateConfig {
@Bean
public Configuration freemarkerConfig() throws TemplateModelException {
Configuration config = new Configuration(Configuration.VERSION_2_3_31);
config.setClassForTemplateLoading(this.getClass(), "/templates");
// 编码设置
config.setDefaultEncoding("UTF-8");
// 数字格式化
config.setNumberFormat("0.######");
// 缓存设置
config.setTemplateUpdateDelayMilliseconds(30000); // 30秒检查一次模板更新
config.setCacheStorage(new freemarker.cache.MruCacheStorage(100, 250));
// 错误处理
config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// 自定义指令
config.setSharedVariable("formatDate", new DateFormatMethod());
config.setSharedVariable("toJson", new ToJsonMethod());
return config;
}
// 自定义日期格式化方法
public static class DateFormatMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arguments) throws TemplateModelException {
if (arguments.size() != 2) {
throw new TemplateModelException("需要两个参数:日期和格式");
}
Object dateObj = arguments.get(0);
Object formatObj = arguments.get(1);
if (dateObj instanceof Date && formatObj instanceof SimpleScalar) {
Date date = (Date) dateObj;
String format = ((SimpleScalar) formatObj).getAsString();
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.format(date);
}
return dateObj.toString();
}
}
// JSON转换方法
public static class ToJsonMethod implements TemplateMethodModelEx {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Object exec(List arguments) throws TemplateModelException {
if (arguments.size() != 1) {
throw new TemplateModelException("需要一个参数:要转换的对象");
}
try {
return objectMapper.writeValueAsString(arguments.get(0));
} catch (Exception e) {
throw new TemplateModelException("JSON转换失败: " + e.getMessage());
}
}
}
}
3. 模板安全管理
java
@Component
public class TemplateSecurityManager {
public Configuration createSecureConfig() throws TemplateModelException {
Configuration config = new Configuration(Configuration.VERSION_2_3_31);
// 限制模板中的Java对象访问
config.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
// 禁用危险的方法调用
config.setSetting("freemarker.template.utility.Execute", "disabled");
config.setSetting("freemarker.template.utility.ObjectConstructor", "disabled");
// 限制可访问的包
config.setSetting("freemarker.ext.beans.BeansWrapper", "restricted");
return config;
}
public boolean validateTemplate(String templateContent) {
// 检查模板中是否包含危险代码
String[] dangerousPatterns = {
"new\\s*java\\.io",
"System\\.exit",
"Runtime\\.getRuntime",
"ProcessBuilder",
"Class\\.forName",
"Method\\.invoke"
};
for (String pattern : dangerousPatterns) {
if (Pattern.compile(pattern).matcher(templateContent).find()) {
return false;
}
}
return true;
}
}
总结
Spring Boot模板引擎为后端开发提供了强大的自动化生成能力,有效解决了重复性工作的痛点,通过掌握Spring Boot模板引擎,开发者能够构建高效的自动化工具链,将更多精力投入到业务逻辑的实现中,提升整体开发效率和代码质量。