Spring Cloud实战小贴士:Zuul统一异常处理(二)

简介: 在前几天发布的《Spring Cloud实战小贴士:Zuul统一异常处理(一)》一文中,我们详细说明了当Zuul的过滤器中抛出异常时会发生客户端没有返回任何内容的问题以及针对这个问题的两种解决方案:一种是通过在各个阶段的过滤器中增加try-catch块,实现过滤器内部的异常处理;另一种是利用error类型过滤器的生命周期特性,集中地处理pre、route、post阶段抛出的异常信息。

在前几天发布的《Spring Cloud实战小贴士:Zuul统一异常处理(一)》一文中,我们详细说明了当Zuul的过滤器中抛出异常时会发生客户端没有返回任何内容的问题以及针对这个问题的两种解决方案:一种是通过在各个阶段的过滤器中增加try-catch块,实现过滤器内部的异常处理;另一种是利用error类型过滤器的生命周期特性,集中地处理preroutepost阶段抛出的异常信息。通常情况下,我们可以将这两种手段同时使用,其中第一种是对开发人员的基本要求;而第二种是对第一种处理方式的补充,以防止一些意外情况的发生。这样的异常处理机制看似已经完美,但是如果在多一些应用实践或源码分析之后,我们会发现依然存在一些不足。

不足之处

下面,我们不妨跟着源码来看看,到底上面的方案还有哪些不足之处需要我们注意和进一步优化的。先来看看外部请求到达API网关服务之后,各个阶段的过滤器是如何进行调度的:

try {
    preRoute();
} catch (ZuulException e) {
    error(e);
    postRoute();
    return;
}
try {
    route();
} catch (ZuulException e) {
    error(e);
    postRoute();
    return;
}
try {
    postRoute();
} catch (ZuulException e) {
    error(e);
    return;
}

上面代码源自com.netflix.zuul.http.ZuulServletservice方法实现,它定义了Zuul处理外部请求过程时,各个类型过滤器的执行逻辑。从代码中我们可以看到三个try-catch块,它们依次分别代表了preroutepost三个阶段的过滤器调用,在catch的异常处理中我们可以看到它们都会被error类型的过滤器进行处理(之前使用error过滤器来定义统一的异常处理也正是利用了这个特性);error类型的过滤器处理完毕之后,除了来自post阶段的异常之外,都会再被post过滤器进行处理。而对于从post过滤器中抛出异常的情况,在经过了error过滤器处理之后,就没有其他类型的过滤器来接手了,这就是使用之前所述方案存在不足之处的根源。

问题分析与进一步优化

回想一下之前实现的两种异常处理方法,其中非常核心的一点,这两种处理方法都在异常处理时候往请求上下文中添加了一系列的error.*参数,而这些参数真正起作用的地方是在post阶段的SendErrorFilter,在该过滤器中会使用这些参数来组织内容返回给客户端。而对于post阶段抛出异常的情况下,由error过滤器处理之后并不会在调用post阶段的请求,自然这些error.*参数也就不会被SendErrorFilter消费输出。所以,如果我们在自定义post过滤器的时候,没有正确的处理异常,就依然有可能出现日志中没有异常并且请求响应内容为空的问题。我们可以通过修改之前ThrowExceptionFilterfilterType修改为post来验证这个问题的存在,注意去掉try-catch块的处理,让它能够抛出异常。

解决上述问题的方法有很多种,比如最直接的我们可以在实现error过滤器的时候,直接来组织结果返回就能实现效果,但是这样的缺点也很明显,对于错误信息组织和返回的代码实现就会存在多份,这样非常不易于我们日后的代码维护工作。所以为了保持对异常返回处理逻辑的一致,我们还是希望将post过滤器抛出的异常能够交给SendErrorFilter来处理。

在前文中,我们已经实现了一个ErrorFilter来捕获preroutepost过滤器抛出的异常,并组织error.*参数保存到请求的上下文中。由于我们的目标是沿用SendErrorFilter,这些error.*参数依然对我们有用,所以我们可以继续沿用该过滤器,让它在post过滤器抛出异常的时候,继续组织error.*参数,只是这里我们已经无法将这些error.*参数再传递给SendErrorFitler过滤器来处理了。所以,我们需要在ErrorFilter过滤器之后再定义一个error类型的过滤器,让它来实现SendErrorFilter的功能,但是这个error过滤器并不需要处理所有出现异常的情况,它仅对post过滤器抛出的异常才有效。根据上面的思路,我们完全可以创建一个继承自SendErrorFilter的过滤器,就能复用它的run方法,然后重写它的类型、顺序以及执行条件,实现对原有逻辑的复用,具体实现如下:

@Component
public class ErrorExtFilter extends SendErrorFilter {

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30;  // 大于ErrorFilter的值
    }

    @Override
    public boolean shouldFilter() {
        // TODO 判断:仅处理来自post过滤器引起的异常
        return true;
    }

}

到这里,我们在过滤器调度上的实现思路已经很清晰了,但是又有一个问题出现在我们面前:怎么判断引起异常的过滤器是来自什么阶段呢?(shouldFilter方法该如何实现)对于这个问题,我们第一反应会寄希望于请求上下文RequestContext对象,可是在查阅文档和源码后发现其中并没有存储异常来源的内容,所以我们不得不扩展原来的过滤器处理逻辑,当有异常抛出的时候,记录下抛出异常的过滤器,这样我们就可以在ErrorExtFilter过滤器的shouldFilter方法中获取并以此判断异常是否来自post阶段的过滤器了。

为了扩展过滤器的处理逻辑,为请求上下文增加一些自定义属性,我们需要深入了解一下Zuul过滤器的核心处理器:com.netflix.zuul.FilterProcessor。该类中定义了下面过滤器调用和处理相关的核心方法:

  • getInstance():该方法用来获取当前处理器的实例
  • setProcessor(FilterProcessor processor):该方法用来设置处理器实例,可以使用此方法来设置自定义的处理器
  • processZuulFilter(ZuulFilter filter):该方法定义了用来执行filter的具体逻辑,包括对请求上下文的设置,判断是否应该执行,执行时一些异常处理等
  • getFiltersByType(String filterType):该方法用来根据传入的filterType获取API网关中对应类型的过滤器,并根据这些过滤器的filterOrder从小到大排序,组织成一个列表返回
  • runFilters(String sType):该方法会根据传入的filterType来调用getFiltersByType(String filterType)获取排序后的过滤器列表,然后轮询这些过滤器,并调用processZuulFilter(ZuulFilter filter)来依次执行它们
  • preRoute():调用runFilters("pre")来执行所有pre类型的过滤器
  • route():调用runFilters("route")来执行所有route类型的过滤器
  • postRoute():调用runFilters("post")来执行所有post类型的过滤器
  • error():调用runFilters("error")来执行所有error类型的过滤器

根据我们之前的设计,我们可以直接通过扩展processZuulFilter(ZuulFilter filter)方法,当过滤器执行抛出异常的时候,我们捕获它,并往请求上下中记录一些信息。比如下面的具体实现:

public class DidiFilterProcessor extends FilterProcessor {

    @Override
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        try {
            return super.processZuulFilter(filter);
        } catch (ZuulException e) {
            RequestContext ctx = RequestContext.getCurrentContext();
            ctx.set("failed.filter", filter);
            throw e;
        }
    }
}

在上面代码的实现中,我们创建了一个FilterProcessor的子类,并重写了processZuulFilter(ZuulFilter filter),虽然主逻辑依然使用了父类的实现,但是在最外层,我们为其增加了异常捕获,并在异常处理中为请求上下文添加了failed.filter属性,以存储抛出异常的过滤器实例。在实现了这个扩展之后,我们也就可以完善之前ErrorExtFilter中的shouldFilter()方法,通过从请求上下文中获取该信息作出正确的判断,具体实现如下:

@Component
public class ErrorExtFilter extends SendErrorFilter {

    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 30;  // 大于ErrorFilter的值
    }

    @Override
    public boolean shouldFilter() {
        // 判断:仅处理来自post过滤器引起的异常
        RequestContext ctx = RequestContext.getCurrentContext();
        ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter");
        if(failedFilter != null && failedFilter.filterType().equals("post")) {
            return true;
        }
        return false;
    }

}

到这里,我们的优化任务还没有完成,因为扩展的过滤器处理类并还没有生效。最后,我们需要在应用主类中,通过调用FilterProcessor.setProcessor(new DidiFilterProcessor());方法来启用自定义的核心处理器以完成我们的优化目标。

本文节选自《Spring Cloud微服务实战》并稍做加工,转载请注明出处

本文由 程序猿DD-翟永超 创作,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。
原文首发于:http://blog.didispace.com/spring-cloud-zuul-exception-2/

目录
相关文章
|
5月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
6月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1089 3
|
4月前
|
监控 Cloud Native Java
Spring Boot 3.x 微服务架构实战指南
🌟蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕Spring Boot 3.x与微服务架构,探索云原生、性能优化与高可用系统设计。以代码为笔,在二进制星河中谱写极客诗篇。关注我,共赴技术星辰大海!(238字)
Spring Boot 3.x 微服务架构实战指南
|
4月前
|
XML Java 测试技术
《深入理解Spring》:IoC容器核心原理与实战
Spring IoC通过控制反转与依赖注入实现对象间的解耦,由容器统一管理Bean的生命周期与依赖关系。支持XML、注解和Java配置三种方式,结合作用域、条件化配置与循环依赖处理等机制,提升应用的可维护性与可测试性,是现代Java开发的核心基石。
|
11月前
|
人工智能 搜索推荐 Java
Spring AI与DeepSeek实战三:打造企业知识库
本文基于Spring AI与RAG技术结合,通过构建实时知识库增强大语言模型能力,实现企业级智能搜索场景与个性化推荐,攻克LLM知识滞后与生成幻觉两大核心痛点。
1272 7
|
9月前
|
人工智能 Java API
Spring AI 实战|Spring AI入门之DeepSeek调用
本文介绍了Spring AI框架如何帮助Java开发者轻松集成和使用大模型API。文章从Spring AI的初探开始,探讨了其核心能力及应用场景,包括手动与自动发起请求、流式响应实现打字机效果,以及兼容不同AI服务(如DeepSeek、通义千问)的方法。同时,还详细讲解了如何在生产环境中添加监控以优化性能和成本管理。通过Spring AI,开发者可以简化大模型调用流程,降低复杂度,为企业智能应用开发提供强大支持。最后,文章展望了Spring AI在未来AI时代的重要作用,鼓励开发者积极拥抱这一技术变革。
3256 71
Spring AI 实战|Spring AI入门之DeepSeek调用
|
10月前
|
安全 Java 数据库
Spring Security 实战指南:从入门到精通
本文详细介绍了Spring Security在Java Web项目中的应用,涵盖登录、权限控制与安全防护等功能。通过Filter Chain过滤器链实现请求拦截与认证授权,核心组件包括AuthenticationProvider和UserDetailsService,负责用户信息加载与密码验证。文章还解析了项目结构,如SecurityConfig配置类、User实体类及自定义登录逻辑,并探讨了Method-Level Security、CSRF防护、Remember-Me等进阶功能。最后总结了Spring Security的核心机制与常见配置,帮助开发者构建健壮的安全系统。
1366 0
|
6月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。