微服务:zuul统一异常处理

我们详细介绍了Spring Cloud Zuul中自己实现的一些核心过滤器,以及这些过滤器在请求生命周期中的不同作用。我们会发现在这些核心过滤器中并没有实现error阶段的过滤器。那么这些过滤器可以用来做什么呢?接下来,本文将介绍如何利用error过滤器来实现统一的异常处理。

过滤器中抛出异常的问题

首先,我们可以来看看默认情况下,过滤器中抛出异常Spring Cloud Zuul会发生什么现象。我们创建一个pre类型的过滤器,并在该过滤器的run方法实现中抛出一个异常。比如下面的实现,在run方法中调用的doSomething方法将抛出RuntimeException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.baoguoding.apigateway;

import com.netflix.zuul.ZuulFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ThrowExceptionFilter extends ZuulFilter {

    private static Logger log = LoggerFactory.getLogger(ThrowExceptionFilter.class);

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

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        log.info("This is a pre filter, it will throw a RuntimeException");
        doSomething();
        return null;
    }

    private void doSomething() {
        throw new RuntimeException("Exist some errors...");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.baoguoding.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;


@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

	@Bean
	public AccessFilter accessFilter() {
		return new AccessFilter();
	}

	@Bean
	public ThrowExceptionFilter throwExceptionFilter() {
		return new ThrowExceptionFilter();
	}

	public static void main(String[] args) {
		SpringApplication.run(ApiGatewayApplication.class, args);
	}

}

运行网关程序并访问某个路由请求,此时我们会发现:在API网关服务的控制台中输出了ThrowExceptionFilter的过滤逻辑中的日志信息,但是并没有输出任何异常信息,同时发起的请求也没有获得任何响应结果。为什么会出现这样的情况呢?我们又该如何在过滤器中处理异常呢?

解决方案一:严格的try-catch处理

运行网关程序并访问某个路由请求,此时我们会发现:在API网关服务的控制台中输出了ThrowExceptionFilter的过滤逻辑中的日志信息,但是并没有输出任何异常信息,同时发起的请求也没有获得任何响应结果。为什么会出现这样的情况呢?我们又该如何在过滤器中处理异常呢?

回想一下,我们在上一节中介绍的所有核心过滤器,是否还记得有一个post过滤器SendErrorFilter是用来处理异常信息的?根据正常的处理流程,该过滤器会处理异常信息,那么这里没有出现任何异常信息说明很有可能就是这个过滤器没有被执行。所以,我们不妨来详细看看SendErrorFilter的shouldFilter函数:

1
2
3
4
5
6
7
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        // only forward to errorPath if it hasn't been forwarded to already
        return ctx.containsKey("error.status_code")
                && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
    }

可以看到该方法的返回值中有一个重要的判断依据ctx.containsKey(“error.status_code”),也就是说请求上下文中必须有error.status_code参数,我们实现的ThrowExceptionFilter中并没有设置这个参数,所以自然不会进入SendErrorFilter过滤器的处理逻辑。那么我们要如何用这个参数呢?我们可以看一下route类型的几个过滤器,由于这些过滤器会对外发起请求,所以肯定会有异常需要处理,比如spring-cloud-netflix-core-1.1.4.RELEASE.jar中的org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter的run方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        this.helper.addIgnoredHeaders();
        try {
            RibbonCommandContext commandContext = buildCommandContext(context);
            ClientHttpResponse response = forward(commandContext);
            setResponse(response);
            return response;
        }
        catch (ZuulException ex) {
            context.set(ERROR_STATUS_CODE, ex.nStatusCode);
            context.set("error.message", ex.errorCause);
            context.set("error.exception", ex);
        }
        catch (Exception ex) {
            context.set("error.status_code",
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            context.set("error.exception", ex);
        }
        return null;
    }

可以看到,整个发起请求的逻辑都采用了try-catch块处理。在catch异常的处理逻辑中并没有做任何输出操作,而是往请求上下文中添加一些error相关的参数,主要有下面三个参数:

其中,error.status_code参数就是SendErrorFilter过滤器用来判断是否需要执行的重要参数。分析到这里,实现异常处理的大致思路就开始明朗了,我们可以参考RibbonRoutingFilter的实现对ThrowExceptionFilter的run方法做一些异常处理的改造,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
    @Override
    public Object run() {
        log.info("This is a pre filter, it will throw a RuntimeException");
        RequestContext ctx = RequestContext.getCurrentContext();
        try {
            doSomething();
        } catch (Exception e) {
            ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            ctx.set("error.exception", e);
        }
        return null;
    }

参考

微服务:过滤器

Zuul的核心是一系列的过滤器,这些过滤器可以完成以下功能:

有4类可重写的方法来自定义过滤器,如下:

请求生命周期

对于其他一些过滤类型,这里就不一一展开了,根据之前对filterType生命周期介绍,可以参考下图去理解,并根据自己的需要在不同的生命周期中去实现不同类型的过滤器。

Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期:

在完成了pre类型的过滤器处理之后,请求进入第二个阶段routing,也就是之前说的路由请求转发阶段,请求将会被routing类型过滤器处理,这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例将请求结果都返回之后,routing阶段完成,请求进入第三个阶段post,此时请求将会被post类型的过滤器进行处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,我们可以对处理结果进行一些加工或转换等内容。

另外,还有一个特殊的阶段error,该阶段只有在上述三个阶段中发生异常的时候才会触发,但是它的最后流向还是post类型的过滤器,因为它需要通过post过滤器将最终结果返回给请求客户端(实际实现上还有一些差别,后续介绍)。

待更新!

参考

微服务:zuul入门介绍

单实例配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.baoguoding.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;


@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(ApiGatewayApplication.class, args);
	}

}

1
2
3
4
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/

多实例配置

1
2
3
4
5
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
#zuul.routes.api-test.url=http://localhost:2223/
api-test.ribbon.listOfServers=http://localhost:2223/,http://localhost:2221/

通过这种方式实现基本的负载均衡

**通过serviceId来映射

1
2
3
4
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.baoguoding.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;


@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(ApiGatewayApplication.class, args);
	}

}

1
2
3
4
5
6
7
8
9
10
11
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/
#api-test.ribbon.listOfServers=http://localhost:2223/,http://localhost:2221/

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=COMPUTE-SERVICE
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=COMPUTE-SERVICE-B
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

服务过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.baoguoding.apigateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;


@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

	@Bean
	public AccessFilter accessFilter() {
		return new AccessFilter();
	}

	public static void main(String[] args) {
		SpringApplication.run(ApiGatewayApplication.class, args);
	}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.baoguoding.apigateway;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;


public class AccessFilter  extends ZuulFilter {

    private static Logger log = LoggerFactory.getLogger(AccessFilter.class);

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

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));

        Object accessToken = request.getParameter("accessToken");
        if (accessToken == null) {
            log.warn("access token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        }
        log.info("access token ok");
        return null;
    }

}

可以有正确的返回结果

最后,总结一下为什么服务网关是微服务架构的重要部分,是我们必须要去做的原因:

参考

微服务:在Spring Cloud中使用Consul实现服务的注册和发现

首先安装consul环境,参照之前的文章:《服务注册发现consul之一:consul介绍及安装》中的第一节介绍。

我们配置三个服务器:

consul-server1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.baoguoding.consulserver1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class ConsulServer1Application {

	@RequestMapping("/home")
	public Object home() {
		System.out.println("cloud-server1");
		return "cloud-server1";
	}

	public static void main(String[] args) {
		SpringApplication.run(ConsulServer1Application.class, args);
	}

}

1
2
3
4
5
6
7
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.healthCheckPath=${management.contextPath}/health
spring.cloud.consul.discovery.healthCheckInterval=15s
spring.cloud.consul.discovery.instance-id=consul-server1
spring.application.name=consul-server
server.port=8503

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[
    {
        "ID": "68c9aed5-1bc2-2a9a-7d60-80f8cf87be9c",
        "Node": "baoguo-PC",
        "Address": "127.0.0.1",
        "Datacenter": "dc1",
        "TaggedAddresses": {
            "lan": "127.0.0.1",
            "wan": "127.0.0.1"
        },
        "NodeMeta": {
            "consul-network-segment": ""
        },
        "ServiceKind": "",
        "ServiceID": "consul-server1",
        "ServiceName": "consul-server",
        "ServiceTags": [
            "secure=false"
        ],
        "ServiceAddress": "baoguo-PC",
        "ServiceWeights": {
            "Passing": 1,
            "Warning": 1
        },
        "ServiceMeta": {},
        "ServicePort": 8503,
        "ServiceEnableTagOverride": false,
        "ServiceProxyDestination": "",
        "ServiceProxy": {},
        "ServiceConnect": {},
        "CreateIndex": 12,
        "ModifyIndex": 12
    }
]

consul-server2

参考源代码

consul-client

参考源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[
	{
		"instanceId": "consul-server1",
		"serviceId": "consul-server",
		"host": "baoguo-PC",
		"port": 8503,
		"secure": false,
		"metadata": {
			"secure": "false"
		},
		"uri": "http://baoguo-PC:8503",
		"scheme": null
	},
	{
		"instanceId": "consul-server2",
		"serviceId": "consul-server",
		"host": "baoguo-PC",
		"port": 8504,
		"secure": false,
		"metadata": {
			"secure": "false"
		},
		"uri": "http://baoguo-PC:8504",
		"scheme": null
	}
]

参考

近期文章

相关页面