- Introduction
- Template Caching
- Thread Safety
- Custom Function Library
- Error Handling Strategies
- Performance Optimization
- Integration Patterns
This guide covers advanced topics for getting the most out of JHP in production environments.
JHP automatically caches parsed templates (AST) to improve performance.
- Template is parsed into an Abstract Syntax Tree (AST)
- AST is cached using the normalized file path as key
- Subsequent renders use the cached AST
- No need to parse the template again
JhpEngine engine = new JhpEngine(settings, lib);
// First render - parses and caches
String output1 = engine.render("page.jhp", ctx1);
// Second render - uses cached AST
String output2 = engine.render("page.jhp", ctx2);The cache is per-engine instance. To invalidate:
// Create a new engine instance
JhpEngine newEngine = new JhpEngine(settings, lib);Templates are cached by their absolute normalized path:
// These use the same cache entry:
engine.render("page.jhp", ctx);
engine.render("./page.jhp", ctx);
engine.render("/full/path/to/page.jhp", ctx);Each engine instance maintains its own cache. For applications with many templates:
// Single engine for entire application
public class TemplateService {
private static final JhpEngine engine = createEngine();
public static String render(String template, Context ctx) throws Exception {
return engine.render(template, ctx);
}
}JHP is designed to be thread-safe for concurrent rendering.
- JhpEngine - Safe to share across threads
- FunctionLibrary - Uses ConcurrentHashMap for custom functions
- Settings - Immutable after creation
- Include Stack - Uses ThreadLocal to track includes per thread
- Scope Stack - Each render has its own scope stack
JhpEngine engine = new JhpEngine(settings, lib);
// Safe to use from multiple threads
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int userId = i;
executor.submit(() -> {
try {
Context ctx = new Context();
ctx.add("userId", userId);
String output = engine.render("user-profile.jhp", ctx);
// Process output
} catch (Exception e) {
e.printStackTrace();
}
});
}Each thread should create its own Context:
// WRONG - Don't share Context across threads
Context sharedCtx = new Context();
executor.submit(() -> engine.render("page.jhp", sharedCtx)); // ❌
// RIGHT - Create Context per thread
executor.submit(() -> {
Context ctx = new Context();
ctx.add("data", data);
engine.render("page.jhp", ctx); // ✓
});Create reusable function libraries for your application.
public class MyFunctionLibrary extends FunctionLibrary {
public MyFunctionLibrary() {
super();
registerCustomFunctions();
}
private void registerCustomFunctions() {
// Date formatting
register("formatdate", (args, scopes) -> {
if (args.isEmpty()) return "";
Instant instant = (Instant) args.get(0);
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
return formatter.format(instant);
});
// Currency formatting
register("currency", (args, scopes) -> {
if (args.isEmpty()) return "$0.00";
Number amount = (Number) args.get(0);
return String.format("$%.2f", amount.doubleValue());
});
// Truncate string
register("truncate", (args, scopes) -> {
if (args.size() < 2) return "";
String text = args.get(0).toString();
int length = ((Number) args.get(1)).intValue();
if (text.length() <= length) return text;
return text.substring(0, length) + "...";
});
}
}MyFunctionLibrary lib = new MyFunctionLibrary();
JhpEngine engine = new JhpEngine(settings, lib);public class EcommerceFunctions extends FunctionLibrary {
public EcommerceFunctions() {
super();
// Calculate discount
register("discount", (args, scopes) -> {
Number price = (Number) args.get(0);
Number percent = (Number) args.get(1);
double discounted = price.doubleValue() * (1 - percent.doubleValue() / 100);
return String.format("%.2f", discounted);
});
// Format SKU
register("formatsku", (args, scopes) -> {
String sku = args.get(0).toString();
return sku.toUpperCase().replaceAll("[^A-Z0-9]", "");
});
}
}Settings settings = Settings.builder()
.issueHandleMode(IssueHandleMode.THROW)
.build();
try {
String output = engine.render("template.jhp", ctx);
} catch (Exception e) {
logger.error("Template error: {}", e.getMessage());
throw e; // Propagate to caller
}Settings settings = Settings.builder()
.issueHandleMode(IssueHandleMode.COMMENT)
.build();
try {
String output = engine.render("template.jhp", ctx);
return output;
} catch (Exception e) {
logger.error("Template rendering failed", e);
return renderErrorPage(e);
}public String renderWithFallback(String template, Context ctx) {
try {
return engine.render(template, ctx);
} catch (Exception e) {
logger.warn("Primary template failed, using fallback", e);
try {
return engine.render("fallback.jhp", ctx);
} catch (Exception fallbackError) {
logger.error("Fallback template also failed", fallbackError);
return "<h1>Error</h1><p>Unable to render page</p>";
}
}
}public String renderSafely(List<String> sections, Context ctx) {
StringBuilder result = new StringBuilder();
for (String section : sections) {
try {
result.append(engine.render(section + ".jhp", ctx));
} catch (Exception e) {
logger.error("Section {} failed", section, e);
result.append("<!-- Section ").append(section).append(" failed -->");
}
}
return result.toString();
}// SLOW - Creates new engine for each request
public String render(String template, Context ctx) {
JhpEngine engine = new JhpEngine(settings, lib);
return engine.render(template, ctx);
}
// FAST - Reuse engine instance
private static final JhpEngine engine = new JhpEngine(settings, lib);
public String render(String template, Context ctx) {
return engine.render(template, ctx);
}// SLOW - Multiple small operations
Context ctx = new Context();
ctx.add("var1", value1);
ctx.add("var2", value2);
// ... many more
// FASTER - Batch preparation
Map<String, Object> data = new HashMap<>();
data.put("var1", value1);
data.put("var2", value2);
// ... prepare all data
Context ctx = new Context();
ctx.getContext().putAll(data);// SLOW - Deep nesting
main.jhp → layout.jhp → header.jhp → nav.jhp → menu.jhp
// FASTER - Flatter structure
main.jhp → layout.jhp → header.jhp// SLOW - Converting large POJOs
LargeObject obj = new LargeObject();
ctx.add("data", obj); // Converts all public fields
// FASTER - Use Map with only needed data
Map<String, Object> data = new HashMap<>();
data.put("id", obj.getId());
data.put("name", obj.getName());
ctx.add("data", data);public void benchmarkTemplate() {
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
Context ctx = new Context();
ctx.add("index", i);
engine.render("template.jhp", ctx);
}
long duration = System.nanoTime() - start;
System.out.printf("Average: %.2f ms%n", duration / 1_000_000.0 / 1000);
}@Controller
public class PageController {
private final JhpEngine engine;
public PageController() {
Settings settings = Settings.builder()
.base("src/main/resources/templates")
.build();
FunctionLibrary lib = new FunctionLibrary();
this.engine = new JhpEngine(settings, lib);
}
@GetMapping("/page/{id}")
public ResponseEntity<String> getPage(@PathVariable String id) {
try {
Context ctx = new Context();
ctx.add("pageId", id);
ctx.add("title", "Page " + id);
String html = engine.render("page.jhp", ctx);
return ResponseEntity.ok()
.contentType(MediaType.TEXT_HTML)
.body(html);
} catch (Exception e) {
return ResponseEntity.status(500).body("Error rendering page");
}
}
}public class EmailService {
private final JhpEngine engine;
public EmailService() {
Settings settings = Settings.builder()
.base("email-templates")
.escape(false) // Emails might need raw HTML
.build();
this.engine = new JhpEngine(settings, new FunctionLibrary());
}
public void sendWelcomeEmail(User user) throws Exception {
Context ctx = new Context();
ctx.add("userName", user.getName());
ctx.add("activationLink", generateActivationLink(user));
String htmlBody = engine.render("welcome-email.jhp", ctx);
String textBody = engine.render("welcome-email-text.jhp", ctx);
emailClient.send(user.getEmail(), "Welcome!", htmlBody, textBody);
}
}public class ReportGenerator {
private final JhpEngine engine;
public void generateReport(ReportData data, OutputStream output) throws Exception {
Context ctx = new Context();
ctx.add("reportDate", Instant.now());
ctx.add("data", data.toMap());
ctx.add("summary", data.getSummary());
String html = engine.render("report-template.jhp", ctx);
// Convert HTML to PDF or write directly
output.write(html.getBytes(StandardCharsets.UTF_8));
}
}public class StaticSiteGenerator {
private final JhpEngine engine;
private final Path outputDir;
public void generateSite(List<Page> pages) throws Exception {
for (Page page : pages) {
Context ctx = new Context();
ctx.add("page", page.toMap());
ctx.add("siteName", "My Site");
String html = engine.render("page-layout.jhp", ctx);
Path outputFile = outputDir.resolve(page.getSlug() + ".html");
Files.writeString(outputFile, html);
}
}
}- Review API Reference
- Check out Examples
- Explore the source code for deeper understanding