java-FreeMarker3.4自定义异常处理

java-FreeMarker3.4自定义异常处理

问题背景

最近在项目中使用 FreeMarker 作为模板引擎时,遇到了一个问题:当模板编译过程中出现语法错误或运行时异常时,FreeMarker 默认的行为是直接打印异常堆栈,跟踪了一些内容后发现堆栈的内容只有小部分有用,大部分都是freemarker底层的逻辑调用,对于问题定位没有什么帮助,反而增加了问题定难度。

于是我希望能够自定义异常处理逻辑,在编译出错时,能够提供更友好的错误信息,只展示必要的错误内容,不看堆栈信息。

环境

  • JDK17
  • Spring Boot 3.4.5
  • FreeMarker 2.3.34

问题分析

FreeMarker 默认的异常处理机制(TemplateExceptionHandler)提供了几种策略:

  1. RETHROW_HANDLER:直接重新抛出异常(默认)
  2. DEBUG_HANDLER:打印异常栈信息到标准错误输出
  3. HTML_DEBUG_HANDLER:将异常栈信息格式化为HTML输出
  4. IGNORE_HANDLER:忽略异常,继续执行

但在实际的生产环境中,我们往往需要更灵活的控制,比如:

  • 将异常信息以特定的格式输出到页面
  • 记录详细的异常栈信息到日志文件
  • 根据不同的环境(开发/生产)采用不同的处理策略

解决方案

FreeMarker 提供了自定义异常处理的扩展点,我们可以通过实现 TemplateExceptionHandler 接口来自定义异常处理逻辑。下面是现方案:

1. 创建自定义异常处理器

java 复制代码
@Configuration
public class FreemarkerConfig {
    
    /**
     * FreeMarker配置器
     */
    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer(DbTemplateLoader dbLoader, ResourceTemplateLoader resLoader) {
        FreeMarkerConfigurer fmkConf = new FreeMarkerConfigurer();

        // 设置自定义异常处理器
        Properties properties = new Properties();
        properties.setProperty(
            freemarker.template.Configuration.TEMPLATE_EXCEPTION_HANDLER_KEY,
            MyTemplateExceptionHandler.class.getName()
        );
        fmkConf.setFreemarkerSettings(properties);

        return fmkConf;
    }

    /**
     * 自定义异常处理器
     * 用于在模板编译或执行出错时,提供更友好的错误信息
     */
    public static class MyTemplateExceptionHandler implements TemplateExceptionHandler {
        
        private static final Logger logger = LoggerFactory.getLogger(MyTemplateExceptionHandler.class);
        
        @Override
        public void handleTemplateException(
            TemplateException te, 
            Environment env, 
            Writer writer
        ) throws TemplateException {
            var sbr = new StringBuilder();
            fillExceptionStackTrace(te, sbr, 1);
            try {
                writer.write(sbr.toString());
            } catch (IOException e) {
                throw new TemplateException("Failed to print error message. Cause: " + e, env);
            }
        }
    
        /**
         * 递归构建异常栈信息
         * 
         * @param ex 异常对象
         * @param err 错误信息构建器
         * @param level 当前递归深度(防止无限递归)
         */
               private void fillExceptionStackTrace(Throwable ex, StringBuilder err, int level){
            if(ex==null || level>=5)
                return;
            err.append(" <br> at ").append(ex.getMessage());
            fillExceptionStackTrace(ex.getCause(),err,level++);
        }
    }
}

2. 支持环境区分(增强版)

在实际项目中,我们可能需要在不同环境中使用不同的错误处理策略:

java 复制代码
/**
 * 支持环境区分的异常处理器工厂
 */
@Component
public class TemplateExceptionHandlerFactory {
    
    @Value("${spring.profiles.active:dev}")
    private String activeProfile;
    
    /**
     * 根据环境创建异常处理器
     */
    public TemplateExceptionHandler createHandler() {
        if ("prod".equals(activeProfile)) {
            // 生产环境:只记录日志,不显示详细错误
            return new ProductionExceptionHandler();
        } else {
            // 开发/测试环境:显示详细错误信息
            return new DevelopmentExceptionHandler();
        }
    }
    
    /**
     * 开发环境异常处理器
     */
    private static class DevelopmentExceptionHandler implements TemplateExceptionHandler {
        @Override
        public void handleTemplateException(TemplateException te, Environment env, Writer writer) 
                throws TemplateException {
            // 显示详细错误信息(同上文实现)
            // ...
        }
    }
    
    /**
     * 生产环境异常处理器
     */
    private static class ProductionExceptionHandler implements TemplateExceptionHandler {
        @Override
        public void handleTemplateException(TemplateException te, Environment env, Writer writer) 
                throws TemplateException {
            // 只记录日志,不暴露详细错误
            logger.error("模板执行异常(已屏蔽详细信息)", te);
            
            try {
                writer.write("<div class='error'>系统繁忙,请稍后再试</div>");
            } catch (IOException e) {
                throw new TemplateException("输出错误信息失败", env);
            }
        }
    }
}
相关推荐
Java后端的Ai之路2 小时前
【Python 教程15】-Python和Web
python
侠客行03173 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪3 小时前
深入浅出LangChain4J
java·langchain·llm
子兮曰3 小时前
OpenClaw入门:从零开始搭建你的私有化AI助手
前端·架构·github
冬奇Lab3 小时前
一天一个开源项目(第15篇):MapToPoster - 用代码将城市地图转换为精美的海报设计
python·开源
吴仰晖3 小时前
使用github copliot chat的源码学习之Chromium Compositor
前端
1024小神3 小时前
github发布pages的几种状态记录
前端
老毛肚5 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎6 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
二十雨辰6 小时前
[python]-AI大模型
开发语言·人工智能·python