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

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

在前几天发布《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微服务实战》并稍做加工,转载请注明出处


相关阅读

目录
相关文章
|
1月前
|
XML Java 测试技术
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
这篇文章介绍了Spring5框架的三个新特性:支持@Nullable注解以明确方法返回、参数和属性值可以为空;引入函数式风格的GenericApplicationContext进行对象注册和管理;以及如何整合JUnit5进行单元测试,同时讨论了JUnit4与JUnit5的整合方法,并提出了关于配置文件加载的疑问。
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
|
1月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
1月前
|
XML Java 数据格式
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
这篇文章是Spring5框架的实战教程,主要介绍了如何在Spring的IOC容器中通过XML配置方式使用外部属性文件来管理Bean,特别是数据库连接池的配置。文章详细讲解了创建属性文件、引入属性文件到Spring配置、以及如何使用属性占位符来引用属性文件中的值。
Spring5入门到实战------7、IOC容器-Bean管理XML方式(外部属性文件)
|
1月前
|
NoSQL Java Redis
Redis6入门到实战------ 八、Redis与Spring Boot整合
这篇文章详细介绍了如何在Spring Boot项目中整合Redis,包括在`pom.xml`中添加依赖、配置`application.properties`文件、创建配置类以及编写测试类来验证Redis的连接和基本操作。
Redis6入门到实战------ 八、Redis与Spring Boot整合
|
11天前
|
前端开发 Java UED
"揭秘!如何以戏剧性姿态,利用SpringCloud铸就无懈可击的异常处理铁壁,让你的微服务架构稳如泰山,震撼业界!"
【9月更文挑战第8天】随着微服务架构的普及,Spring Cloud作为一套完整的微服务解决方案被广泛应用。在微服务架构中,服务间调用频繁且复杂,异常处理成为保障系统稳定性和用户体验的关键。传统的异常处理方式导致代码冗余,降低系统可维护性和一致性。因此,基于Spring Cloud封装统一的异常处理机制至关重要。这样不仅可以减少代码冗余、提升一致性,还增强了系统的可维护性,并通过统一的错误响应格式优化了用户体验。具体实现包括定义全局异常处理器、自定义业务异常以及在服务中抛出这些异常。这种方式体现了微服务架构中的“服务治理”和“契约先行”原则,有助于构建健壮、可扩展的系统。
27 2
|
1月前
|
SQL 数据库
Spring5入门到实战------13、使用JdbcTemplate操作数据库(批量增删改)。具体代码+讲解 【下篇】
这篇文章是Spring5框架的实战教程,深入讲解了如何使用JdbcTemplate进行数据库的批量操作,包括批量添加、批量修改和批量删除的具体代码实现和测试过程,并通过完整的项目案例展示了如何在实际开发中应用这些技术。
Spring5入门到实战------13、使用JdbcTemplate操作数据库(批量增删改)。具体代码+讲解 【下篇】
|
1月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
21天前
|
Java API UED
【实战秘籍】Spring Boot开发者的福音:掌握网络防抖动,告别无效请求,提升用户体验!
【8月更文挑战第29天】网络防抖动技术能有效处理频繁触发的事件或请求,避免资源浪费,提升系统响应速度与用户体验。本文介绍如何在Spring Boot中实现防抖动,并提供代码示例。通过使用ScheduledExecutorService,可轻松实现延迟执行功能,确保仅在用户停止输入后才触发操作,大幅减少服务器负载。此外,还可利用`@Async`注解简化异步处理逻辑。防抖动是优化应用性能的关键策略,有助于打造高效稳定的软件系统。
30 2
|
1月前
|
XML Java Maven
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
这篇文章是Spring5框架的入门到实战教程,介绍了Spring5的新功能——整合日志框架Log4j2,包括Spring5对日志框架的通用封装、如何在项目中引入Log4j2、编写Log4j2的XML配置文件,并通过测试类展示了如何使用Log4j2进行日志记录。
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
|
1月前
|
XML Java 数据库
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
这篇文章是Spring5框架的实战教程,详细介绍了事务的概念、ACID特性、事务操作的场景,并通过实际的银行转账示例,演示了Spring框架中声明式事务管理的实现,包括使用注解和XML配置两种方式,以及如何配置事务参数来控制事务的行为。
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式