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);
            }
        }
    }
}
相关推荐
段一凡-华北理工大学5 小时前
工业智能体:从Codex/Claude到项目实战~系列文章05:从“AI助手“到“工业智能体“:能力跃迁与落地范式
人工智能·python·claude·codex·工业智能体·高炉炼铁智能化
独泪了无痕5 小时前
探索Pinyin4j-Java中的拼音转换利器
java·后端
深蓝AI5 小时前
ContextVar 到线程池就丢了?asyncio.to_thread 和 run_in_executor 实测
python
Csvn6 小时前
Monorepo
前端
Csvn6 小时前
流水线
前端
Csvn6 小时前
微前端架构
前端
道尔柯南6 小时前
C++ string 类详解:从常用接口到深拷贝,彻底掌握字符串处理
android·java·c++
岁月宁静7 小时前
什么是 Agent:从"会聊天"到"会做事"
人工智能·python
岁月宁静7 小时前
AI 应用开发工程师简历怎么写:HR 初筛、技术复筛、面试追问,三道关卡各看什么?
人工智能·python·面试
来让爷抱一个7 小时前
Python 异步里混用同步库,事件循环被卡住的全过程
开发语言·python