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);
            }
        }
    }
}
相关推荐
Awu12276 小时前
⚡从零开发 Agent CLI(五)实现一个可治理、可扩展的工具系统
前端·人工智能·claude
咪库咪库咪7 小时前
Vue3-生命周期
前端
莪_幻尘7 小时前
你的 AI Skill 越多越蠢?Token 上下文爆炸的求生指南
前端·ai编程
lichenyang4537 小时前
从 has.echo 到异步 API 注册表:一次 ASCF API 回调不触发的排查复盘
前端
林瞅瞅8 小时前
Nuxt3 项目部署 Nginx 防盗链后特定 JS 文件 403 问题修复方案
前端
kyriewen8 小时前
别再每次都 Google 了:我整理了前端日常最常踩的 10 个 Git 坑,附速查表
前端·javascript·git
一颗奇趣蛋8 小时前
Web 视频开发完全指南:从入门到精通
前端
非洲农业不发达9 小时前
windows终端体验大升级,让你拥有macos级别的美化
前端·后端
妙码生花9 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十七):登录接口完善,登录页接口整合,解决跨域
前端·后端·ai编程
唐诗9 小时前
改 3 行配置,我的 Tauri dev 冷启动从 100 秒干到 4 秒
前端·客户端